Release 202506141856
This commit is contained in:
@@ -96,7 +96,7 @@ Tasks will run according to their configured cron schedules.
|
|||||||
### Force Triggers Task
|
### Force Triggers Task
|
||||||
|
|
||||||
- Maintains database triggers by forcing them to run under certain conditions
|
- Maintains database triggers by forcing them to run under certain conditions
|
||||||
- Currently handles updating the "isRecentlyListed" flag for services after 15 days
|
- Currently handles updating the "isRecentlyApproved" flag for services after 15 days
|
||||||
- Scheduled via `CRON_FORCE-TRIGGERS_TASK`
|
- Scheduled via `CRON_FORCE-TRIGGERS_TASK`
|
||||||
|
|
||||||
### Service Score Recalculation Task
|
### Service Score Recalculation Task
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ def parse_args(args: List[str]) -> argparse.Namespace:
|
|||||||
score_recalc_parser.add_argument(
|
score_recalc_parser.add_argument(
|
||||||
"--service-id", type=int, help="Specific service ID to process (optional)"
|
"--service-id", type=int, help="Specific service ID to process (optional)"
|
||||||
)
|
)
|
||||||
|
score_recalc_parser.add_argument(
|
||||||
|
"--all",
|
||||||
|
action="store_true",
|
||||||
|
help="Recalculate scores for all services (ignores --service-id)",
|
||||||
|
)
|
||||||
|
|
||||||
return parser.parse_args(args)
|
return parser.parse_args(args)
|
||||||
|
|
||||||
@@ -295,12 +300,15 @@ def run_force_triggers_task() -> int:
|
|||||||
close_db_pool()
|
close_db_pool()
|
||||||
|
|
||||||
|
|
||||||
def run_service_score_recalc_task(service_id: Optional[int] = None) -> int:
|
def run_service_score_recalc_task(
|
||||||
|
service_id: Optional[int] = None, all_services: bool = False
|
||||||
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Run the service score recalculation task.
|
Run the service score recalculation task.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service_id: Optional specific service ID to process.
|
service_id: Optional specific service ID to process.
|
||||||
|
all_services: Whether to recalculate scores for all services.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Exit code.
|
Exit code.
|
||||||
@@ -310,7 +318,34 @@ def run_service_score_recalc_task(service_id: Optional[int] = None) -> int:
|
|||||||
try:
|
try:
|
||||||
# Initialize task and use as context manager
|
# Initialize task and use as context manager
|
||||||
with ServiceScoreRecalculationTask() as task: # type: ignore
|
with ServiceScoreRecalculationTask() as task: # type: ignore
|
||||||
result = task.run(service_id) # type: ignore
|
if all_services:
|
||||||
|
queued = task.recalculate_all_services() # type: ignore
|
||||||
|
if not queued:
|
||||||
|
logger.warning(
|
||||||
|
"Failed to queue recalculation jobs for all services"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Continuously process queued jobs in batches until none remain
|
||||||
|
while True:
|
||||||
|
_ = task.run() # type: ignore
|
||||||
|
|
||||||
|
# Check if there are still unprocessed jobs
|
||||||
|
remaining = 0
|
||||||
|
if task.conn:
|
||||||
|
with task.conn.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
'SELECT COUNT(*) FROM "ServiceScoreRecalculationJob" WHERE "processedAt" IS NULL'
|
||||||
|
)
|
||||||
|
remaining = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if remaining == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
result = True # All jobs processed successfully
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = task.run(service_id) # type: ignore
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
logger.info("Successfully recalculated service scores")
|
logger.info("Successfully recalculated service scores")
|
||||||
else:
|
else:
|
||||||
@@ -419,7 +454,9 @@ def main() -> int:
|
|||||||
elif args.task == "force-triggers":
|
elif args.task == "force-triggers":
|
||||||
return run_force_triggers_task()
|
return run_force_triggers_task()
|
||||||
elif args.task == "service-score-recalc":
|
elif args.task == "service-score-recalc":
|
||||||
return run_service_score_recalc_task(args.service_id)
|
return run_service_score_recalc_task(
|
||||||
|
args.service_id, getattr(args, "all", False)
|
||||||
|
)
|
||||||
elif args.task:
|
elif args.task:
|
||||||
logger.error(f"Unknown task: {args.task}")
|
logger.error(f"Unknown task: {args.task}")
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class ForceTriggersTask(Task):
|
|||||||
Force triggers to run under certain conditions.
|
Force triggers to run under certain conditions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RECENT_LISTED_INTERVAL_DAYS = 15
|
RECENT_APPROVED_INTERVAL_DAYS = 15
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("force_triggers")
|
super().__init__("force_triggers")
|
||||||
@@ -24,10 +24,10 @@ class ForceTriggersTask(Task):
|
|||||||
|
|
||||||
update_query = f"""
|
update_query = f"""
|
||||||
UPDATE "Service"
|
UPDATE "Service"
|
||||||
SET "isRecentlyListed" = FALSE, "updatedAt" = NOW()
|
SET "isRecentlyApproved" = FALSE, "updatedAt" = NOW()
|
||||||
WHERE "isRecentlyListed" = TRUE
|
WHERE "isRecentlyApproved" = TRUE
|
||||||
AND "listedAt" IS NOT NULL
|
AND "approvedAt" IS NOT NULL
|
||||||
AND "listedAt" < NOW() - INTERVAL '{self.RECENT_LISTED_INTERVAL_DAYS} days'
|
AND "approvedAt" < NOW() - INTERVAL '{self.RECENT_APPROVED_INTERVAL_DAYS} days'
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with self.conn.cursor() as cursor:
|
with self.conn.cursor() as cursor:
|
||||||
|
|||||||
@@ -206,7 +206,6 @@ class ServiceScoreRecalculationTask(Task):
|
|||||||
"""
|
"""
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM "Service"
|
FROM "Service"
|
||||||
WHERE "isActive" = TRUE
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
services = cursor.fetchall()
|
services = cursor.fetchall()
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" ADD COLUMN "approvedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "spamAt" TIMESTAMP(3);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `isRecentlyListed` on the `Service` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Service" DROP COLUMN "isRecentlyListed",
|
||||||
|
ADD COLUMN "isRecentlyApproved" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Service_approvedAt_idx" ON "Service"("approvedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Service_verifiedAt_idx" ON "Service"("verifiedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Service_spamAt_idx" ON "Service"("spamAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Service_serviceVisibility_idx" ON "Service"("serviceVisibility");
|
||||||
@@ -353,8 +353,6 @@ model Service {
|
|||||||
privacyScore Int @default(0)
|
privacyScore Int @default(0)
|
||||||
trustScore Int @default(0)
|
trustScore Int @default(0)
|
||||||
/// Computed via trigger. Do not update through prisma.
|
/// Computed via trigger. Do not update through prisma.
|
||||||
isRecentlyListed Boolean @default(false)
|
|
||||||
/// Computed via trigger. Do not update through prisma.
|
|
||||||
averageUserRating Float?
|
averageUserRating Float?
|
||||||
serviceVisibility ServiceVisibility @default(PUBLIC)
|
serviceVisibility ServiceVisibility @default(PUBLIC)
|
||||||
serviceInfoBanner ServiceInfoBanner @default(NONE)
|
serviceInfoBanner ServiceInfoBanner @default(NONE)
|
||||||
@@ -363,8 +361,6 @@ model Service {
|
|||||||
verificationSummary String?
|
verificationSummary String?
|
||||||
verificationRequests ServiceVerificationRequest[]
|
verificationRequests ServiceVerificationRequest[]
|
||||||
verificationProofMd String?
|
verificationProofMd String?
|
||||||
/// Computed via trigger when the service status is VERIFICATION_SUCCESS. Do not update through prisma.
|
|
||||||
verifiedAt DateTime?
|
|
||||||
/// [UserSentiment]
|
/// [UserSentiment]
|
||||||
userSentiment Json?
|
userSentiment Json?
|
||||||
userSentimentAt DateTime?
|
userSentimentAt DateTime?
|
||||||
@@ -380,7 +376,16 @@ model Service {
|
|||||||
tosReviewAt DateTime?
|
tosReviewAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
/// Computed via trigger when the visibility is PUBLIC or (ARCHIVED and listedAt was null). Do not update through prisma.
|
||||||
listedAt DateTime?
|
listedAt DateTime?
|
||||||
|
/// Computed via trigger when the verification status is APPROVED. Do not update through prisma.
|
||||||
|
approvedAt DateTime?
|
||||||
|
/// Computed via trigger when the verification status is VERIFICATION_SUCCESS. Do not update through prisma.
|
||||||
|
verifiedAt DateTime?
|
||||||
|
/// Computed via trigger when the verification status is VERIFICATION_FAILED. Do not update through prisma.
|
||||||
|
spamAt DateTime?
|
||||||
|
/// Computed via trigger. Do not update through prisma.
|
||||||
|
isRecentlyApproved Boolean @default(false)
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
events Event[]
|
events Event[]
|
||||||
contactMethods ServiceContactMethod[] @relation("ServiceToContactMethod")
|
contactMethods ServiceContactMethod[] @relation("ServiceToContactMethod")
|
||||||
@@ -396,6 +401,9 @@ model Service {
|
|||||||
affiliatedUsers ServiceUser[] @relation("ServiceUsers")
|
affiliatedUsers ServiceUser[] @relation("ServiceUsers")
|
||||||
|
|
||||||
@@index([listedAt])
|
@@index([listedAt])
|
||||||
|
@@index([approvedAt])
|
||||||
|
@@index([verifiedAt])
|
||||||
|
@@index([spamAt])
|
||||||
@@index([overallScore])
|
@@index([overallScore])
|
||||||
@@index([privacyScore])
|
@@index([privacyScore])
|
||||||
@@index([trustScore])
|
@@index([trustScore])
|
||||||
@@ -407,6 +415,7 @@ model Service {
|
|||||||
@@index([updatedAt])
|
@@index([updatedAt])
|
||||||
@@index([slug])
|
@@index([slug])
|
||||||
@@index([previousSlugs])
|
@@index([previousSlugs])
|
||||||
|
@@index([serviceVisibility])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ServiceContactMethod {
|
model ServiceContactMethod {
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import {
|
|||||||
PrismaClient,
|
PrismaClient,
|
||||||
ServiceSuggestionStatus,
|
ServiceSuggestionStatus,
|
||||||
ServiceUserRole,
|
ServiceUserRole,
|
||||||
VerificationStatus,
|
|
||||||
type Prisma,
|
type Prisma,
|
||||||
type User,
|
type User,
|
||||||
type ServiceVisibility,
|
type ServiceVisibility,
|
||||||
ServiceSuggestionType,
|
ServiceSuggestionType,
|
||||||
KycLevelClarification,
|
KycLevelClarification,
|
||||||
VerificationStepStatus,
|
VerificationStepStatus,
|
||||||
|
type VerificationStatus,
|
||||||
} from '@prisma/client'
|
} from '@prisma/client'
|
||||||
|
import { differenceInDays, isPast } from 'date-fns'
|
||||||
import { omit, uniqBy } from 'lodash-es'
|
import { omit, uniqBy } from 'lodash-es'
|
||||||
import { generateUsername } from 'unique-username-generator'
|
import { generateUsername } from 'unique-username-generator'
|
||||||
|
|
||||||
@@ -614,6 +615,14 @@ const generateFakeService = (users: User[]) => {
|
|||||||
const tosReview = faker.helpers.maybe(() => faker.helpers.arrayElement(tosReviewExamples), {
|
const tosReview = faker.helpers.maybe(() => faker.helpers.arrayElement(tosReviewExamples), {
|
||||||
probability: 0.8,
|
probability: 0.8,
|
||||||
})
|
})
|
||||||
|
const serviceVisibility = faker.helpers.weightedArrayElement<ServiceVisibility>([
|
||||||
|
{ weight: 80, value: 'PUBLIC' },
|
||||||
|
{ weight: 10, value: 'UNLISTED' },
|
||||||
|
{ weight: 5, value: 'HIDDEN' },
|
||||||
|
{ weight: 5, value: 'ARCHIVED' },
|
||||||
|
])
|
||||||
|
const approvedAt =
|
||||||
|
status === 'APPROVED' || status === 'VERIFICATION_SUCCESS' ? faker.date.recent({ days: 30 }) : null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@@ -629,12 +638,7 @@ const generateFakeService = (users: User[]) => {
|
|||||||
overallScore: 0,
|
overallScore: 0,
|
||||||
privacyScore: 0,
|
privacyScore: 0,
|
||||||
trustScore: 0,
|
trustScore: 0,
|
||||||
serviceVisibility: faker.helpers.weightedArrayElement<ServiceVisibility>([
|
serviceVisibility,
|
||||||
{ weight: 80, value: 'PUBLIC' },
|
|
||||||
{ weight: 10, value: 'UNLISTED' },
|
|
||||||
{ weight: 5, value: 'HIDDEN' },
|
|
||||||
{ weight: 5, value: 'ARCHIVED' },
|
|
||||||
]),
|
|
||||||
verificationStatus: status,
|
verificationStatus: status,
|
||||||
verificationSummary:
|
verificationSummary:
|
||||||
status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraph() : null,
|
status === 'VERIFICATION_SUCCESS' || status === 'VERIFICATION_FAILED' ? faker.lorem.paragraph() : null,
|
||||||
@@ -677,8 +681,14 @@ const generateFakeService = (users: User[]) => {
|
|||||||
{ count: { min: 0, max: 2 } }
|
{ count: { min: 0, max: 2 } }
|
||||||
),
|
),
|
||||||
imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random&format=svg`,
|
imageUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=random&format=svg`,
|
||||||
listedAt: faker.date.past(),
|
listedAt:
|
||||||
verifiedAt: status === VerificationStatus.VERIFICATION_SUCCESS ? faker.date.past() : null,
|
serviceVisibility === 'PUBLIC' || serviceVisibility === 'ARCHIVED'
|
||||||
|
? faker.date.recent({ days: 30 })
|
||||||
|
: null,
|
||||||
|
verifiedAt: status === 'VERIFICATION_SUCCESS' ? faker.date.recent({ days: 30 }) : null,
|
||||||
|
spamAt: status === 'VERIFICATION_FAILED' ? faker.date.recent({ days: 30 }) : null,
|
||||||
|
approvedAt,
|
||||||
|
isRecentlyApproved: !!approvedAt && isPast(approvedAt) && differenceInDays(new Date(), approvedAt) < 15,
|
||||||
tosReview,
|
tosReview,
|
||||||
tosReviewAt: tosReview
|
tosReviewAt: tosReview
|
||||||
? faker.date.recent()
|
? faker.date.recent()
|
||||||
@@ -908,7 +918,7 @@ const generateFakeServiceContactMethod = (serviceId: number) => {
|
|||||||
value: `https://linkedin.com/in/${faker.helpers.slugify(faker.person.fullName())}`,
|
value: `https://linkedin.com/in/${faker.helpers.slugify(faker.person.fullName())}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: faker.lorem.word({ length: 2 }),
|
label: 'Custom label',
|
||||||
value: `https://bitcointalk.org/index.php?topic=${faker.number.int({ min: 1, max: 1000000 }).toString()}.0`,
|
value: `https://bitcointalk.org/index.php?topic=${faker.number.int({ min: 1, max: 1000000 }).toString()}.0`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -918,7 +928,7 @@ const generateFakeServiceContactMethod = (serviceId: number) => {
|
|||||||
value: faker.internet.url(),
|
value: faker.internet.url(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: faker.lorem.word({ length: 2 }),
|
label: 'Custom label',
|
||||||
value: faker.internet.url(),
|
value: faker.internet.url(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1307,7 +1317,7 @@ async function main() {
|
|||||||
const service = await prisma.service.create({
|
const service = await prisma.service.create({
|
||||||
data: {
|
data: {
|
||||||
...serviceData,
|
...serviceData,
|
||||||
verificationStatus: VerificationStatus.COMMUNITY_CONTRIBUTED,
|
verificationStatus: 'COMMUNITY_CONTRIBUTED',
|
||||||
categories: {
|
categories: {
|
||||||
connect: randomCategories.map((cat) => ({ id: cat.id })),
|
connect: randomCategories.map((cat) => ({ id: cat.id })),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
-- This script defines PostgreSQL functions and triggers for managing service scores:
|
-- This script defines PostgreSQL functions and triggers for managing service scores:
|
||||||
-- 1. Automatically calculates and updates privacy, trust, and overall scores
|
-- 1. Automatically calculates and updates privacy, trust, and overall scores
|
||||||
-- for services when services or their attributes change.
|
-- for services when services or their attributes change.
|
||||||
-- 2. Updates the isRecentlyListed flag for services listed within the last 15 days.
|
-- 2. Updates the isRecentlyApproved flag for services approved within the last 15 days.
|
||||||
-- 3. Queues asynchronous score recalculation in "ServiceScoreRecalculationJob"
|
-- 3. Queues asynchronous score recalculation in "ServiceScoreRecalculationJob"
|
||||||
-- when an "Attribute" definition (e.g., points) is updated, ensuring
|
-- when an "Attribute" definition (e.g., points) is updated, ensuring
|
||||||
-- efficient handling of widespread score updates.
|
-- efficient handling of widespread score updates.
|
||||||
@@ -25,12 +25,8 @@ DECLARE
|
|||||||
privacy_score INT := 0;
|
privacy_score INT := 0;
|
||||||
kyc_factor INT;
|
kyc_factor INT;
|
||||||
clarification_factor INT := 0;
|
clarification_factor INT := 0;
|
||||||
onion_factor INT := 0;
|
onion_or_i2p_factor INT := 0;
|
||||||
i2p_factor INT := 0;
|
|
||||||
monero_factor INT := 0;
|
monero_factor INT := 0;
|
||||||
open_source_factor INT := 0;
|
|
||||||
p2p_factor INT := 0;
|
|
||||||
decentralized_factor INT := 0;
|
|
||||||
attributes_score INT := 0;
|
attributes_score INT := 0;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Get service data
|
-- Get service data
|
||||||
@@ -57,20 +53,12 @@ BEGIN
|
|||||||
FROM "Service"
|
FROM "Service"
|
||||||
WHERE "id" = service_id;
|
WHERE "id" = service_id;
|
||||||
|
|
||||||
-- Check for onion URLs
|
-- Check for onion or i2p URLs
|
||||||
IF EXISTS (
|
IF EXISTS (
|
||||||
SELECT 1 FROM "Service"
|
SELECT 1 FROM "Service"
|
||||||
WHERE "id" = service_id AND array_length("onionUrls", 1) > 0
|
WHERE "id" = service_id AND (array_length("onionUrls", 1) > 0 OR array_length("i2pUrls", 1) > 0)
|
||||||
) THEN
|
) THEN
|
||||||
onion_factor := 5;
|
onion_or_i2p_factor := 5;
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Check for i2p URLs
|
|
||||||
IF EXISTS (
|
|
||||||
SELECT 1 FROM "Service"
|
|
||||||
WHERE "id" = service_id AND array_length("i2pUrls", 1) > 0
|
|
||||||
) THEN
|
|
||||||
i2p_factor := 5;
|
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Check for Monero acceptance
|
-- Check for Monero acceptance
|
||||||
@@ -86,10 +74,10 @@ BEGIN
|
|||||||
INTO attributes_score
|
INTO attributes_score
|
||||||
FROM "ServiceAttribute" sa
|
FROM "ServiceAttribute" sa
|
||||||
JOIN "Attribute" a ON sa."attributeId" = a."id"
|
JOIN "Attribute" a ON sa."attributeId" = a."id"
|
||||||
WHERE sa."serviceId" = service_id AND a."category" = 'PRIVACY';
|
WHERE sa."serviceId" = service_id;
|
||||||
|
|
||||||
-- Calculate final privacy score (base 100)
|
-- Calculate final privacy score (base 100)
|
||||||
privacy_score := 50 + kyc_factor + clarification_factor + onion_factor + i2p_factor + monero_factor + open_source_factor + p2p_factor + decentralized_factor + attributes_score;
|
privacy_score := 50 + kyc_factor + clarification_factor + onion_or_i2p_factor + monero_factor + attributes_score;
|
||||||
|
|
||||||
-- Ensure the score is in reasonable bounds (0-100)
|
-- Ensure the score is in reasonable bounds (0-100)
|
||||||
privacy_score := GREATEST(0, LEAST(100, privacy_score));
|
privacy_score := GREATEST(0, LEAST(100, privacy_score));
|
||||||
@@ -105,7 +93,7 @@ DECLARE
|
|||||||
trust_score INT := 0;
|
trust_score INT := 0;
|
||||||
verification_factor INT;
|
verification_factor INT;
|
||||||
attributes_score INT := 0;
|
attributes_score INT := 0;
|
||||||
recently_listed_factor INT := 0;
|
recently_approved_factor INT := 0;
|
||||||
tos_penalty_factor INT := 0;
|
tos_penalty_factor INT := 0;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Get verification status factor
|
-- Get verification status factor
|
||||||
@@ -126,26 +114,26 @@ BEGIN
|
|||||||
INTO attributes_score
|
INTO attributes_score
|
||||||
FROM "ServiceAttribute" sa
|
FROM "ServiceAttribute" sa
|
||||||
JOIN "Attribute" a ON sa."attributeId" = a.id
|
JOIN "Attribute" a ON sa."attributeId" = a.id
|
||||||
WHERE sa."serviceId" = service_id AND a.category = 'TRUST';
|
WHERE sa."serviceId" = service_id;
|
||||||
|
|
||||||
-- Apply penalty if service was listed within the last 15 days
|
-- Apply penalty if service was approved within the last 15 days
|
||||||
IF EXISTS (
|
IF EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM "Service"
|
FROM "Service"
|
||||||
WHERE id = service_id
|
WHERE id = service_id
|
||||||
AND "listedAt" IS NOT NULL
|
AND "approvedAt" IS NOT NULL
|
||||||
AND "verificationStatus" = 'APPROVED'
|
AND "verificationStatus" = 'APPROVED'
|
||||||
AND (NOW() - "listedAt") <= INTERVAL '15 days'
|
AND (NOW() - "approvedAt") <= INTERVAL '15 days'
|
||||||
) THEN
|
) THEN
|
||||||
recently_listed_factor := -10;
|
recently_approved_factor := -10;
|
||||||
-- Update the isRecentlyListed flag to true
|
-- Update the isRecentlyApproved flag to true
|
||||||
UPDATE "Service"
|
UPDATE "Service"
|
||||||
SET "isRecentlyListed" = TRUE
|
SET "isRecentlyApproved" = TRUE
|
||||||
WHERE id = service_id;
|
WHERE id = service_id;
|
||||||
ELSE
|
ELSE
|
||||||
-- Update the isRecentlyListed flag to false
|
-- Update the isRecentlyApproved flag to false
|
||||||
UPDATE "Service"
|
UPDATE "Service"
|
||||||
SET "isRecentlyListed" = FALSE
|
SET "isRecentlyApproved" = FALSE
|
||||||
WHERE id = service_id;
|
WHERE id = service_id;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
@@ -161,7 +149,7 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
-- Calculate final trust score (base 100)
|
-- Calculate final trust score (base 100)
|
||||||
trust_score := 50 + verification_factor + attributes_score + recently_listed_factor + tos_penalty_factor;
|
trust_score := 50 + verification_factor + attributes_score + recently_approved_factor + tos_penalty_factor;
|
||||||
|
|
||||||
-- Ensure the score is in reasonable bounds (0-100)
|
-- Ensure the score is in reasonable bounds (0-100)
|
||||||
trust_score := GREATEST(0, LEAST(100, trust_score));
|
trust_score := GREATEST(0, LEAST(100, trust_score));
|
||||||
@@ -176,7 +164,7 @@ RETURNS INT AS $$
|
|||||||
DECLARE
|
DECLARE
|
||||||
overall_score INT;
|
overall_score INT;
|
||||||
BEGIN
|
BEGIN
|
||||||
overall_score := CAST(ROUND(((privacy_score * 0.6) + (trust_score * 0.4)) / 10.0) AS INT);
|
overall_score := CAST(((privacy_score * 0.6) + (trust_score * 0.4)) / 10.0 AS INT);
|
||||||
RETURN GREATEST(0, LEAST(10, overall_score));
|
RETURN GREATEST(0, LEAST(10, overall_score));
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|||||||
@@ -1,48 +1,60 @@
|
|||||||
-- This script manages the `listedAt`, `verifiedAt`, and `isRecentlyListed` timestamps
|
CREATE OR REPLACE FUNCTION manage_service_visibility_timestamps()
|
||||||
-- for services based on changes to their `verificationStatus`. It ensures these timestamps
|
|
||||||
-- are set or cleared appropriately when a service's verification status is updated.
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION manage_service_timestamps()
|
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Manage listedAt timestamp
|
IF NEW."serviceVisibility" = 'PUBLIC' OR NEW."serviceVisibility" = 'ARCHIVED' THEN
|
||||||
IF NEW."verificationStatus" IN ('APPROVED', 'VERIFICATION_SUCCESS') THEN
|
|
||||||
-- Set listedAt only on the first time status becomes APPROVED or VERIFICATION_SUCCESS
|
|
||||||
IF OLD."listedAt" IS NULL THEN
|
IF OLD."listedAt" IS NULL THEN
|
||||||
NEW."listedAt" := NOW();
|
NEW."listedAt" := NOW();
|
||||||
NEW."isRecentlyListed" := TRUE;
|
|
||||||
END IF;
|
END IF;
|
||||||
ELSIF OLD."verificationStatus" IN ('APPROVED', 'VERIFICATION_SUCCESS') THEN
|
ELSE
|
||||||
-- Clear listedAt if the status changes FROM APPROVED or VERIFICATION_SUCCESS to something else
|
|
||||||
-- The trigger's WHEN clause ensures NEW."verificationStatus" is different.
|
|
||||||
NEW."listedAt" := NULL;
|
NEW."listedAt" := NULL;
|
||||||
NEW."isRecentlyListed" := FALSE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- Manage verifiedAt timestamp
|
|
||||||
IF NEW."verificationStatus" = 'VERIFICATION_SUCCESS' THEN
|
|
||||||
-- Set verifiedAt when status changes TO VERIFICATION_SUCCESS
|
|
||||||
NEW."verifiedAt" := NOW();
|
|
||||||
NEW."isRecentlyListed" := FALSE;
|
|
||||||
ELSIF OLD."verificationStatus" = 'VERIFICATION_SUCCESS' THEN
|
|
||||||
-- Clear verifiedAt when status changes FROM VERIFICATION_SUCCESS
|
|
||||||
-- The trigger's WHEN clause ensures NEW."verificationStatus" is different.
|
|
||||||
NEW."verifiedAt" := NULL;
|
|
||||||
NEW."isRecentlyListed" := FALSE;
|
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- Drop the old trigger first if it exists under the old name
|
CREATE OR REPLACE FUNCTION manage_service_verification_timestamps()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF (NEW."verificationStatus" = 'APPROVED' OR NEW."verificationStatus" = 'VERIFICATION_SUCCESS') THEN
|
||||||
|
IF OLD."approvedAt" IS NULL THEN
|
||||||
|
NEW."approvedAt" := NOW();
|
||||||
|
NEW."isRecentlyApproved" := TRUE;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
NEW."approvedAt" := NULL;
|
||||||
|
NEW."isRecentlyApproved" := FALSE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW."verificationStatus" = 'VERIFICATION_SUCCESS' THEN
|
||||||
|
NEW."verifiedAt" := NOW();
|
||||||
|
ELSE
|
||||||
|
NEW."verifiedAt" := NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NEW."verificationStatus" = 'VERIFICATION_FAILED' THEN
|
||||||
|
NEW."spamAt" := NOW();
|
||||||
|
ELSE
|
||||||
|
NEW."spamAt" := NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Drop the old triggers TODO: remove this some day
|
||||||
DROP TRIGGER IF EXISTS trigger_set_service_listed_at ON "Service";
|
DROP TRIGGER IF EXISTS trigger_set_service_listed_at ON "Service";
|
||||||
-- Drop the trigger if it exists under the new name
|
|
||||||
DROP TRIGGER IF EXISTS trigger_manage_service_timestamps ON "Service";
|
DROP TRIGGER IF EXISTS trigger_manage_service_timestamps ON "Service";
|
||||||
|
|
||||||
CREATE TRIGGER trigger_manage_service_timestamps
|
DROP TRIGGER IF EXISTS trigger_manage_service_visibility_timestamps ON "Service";
|
||||||
|
DROP TRIGGER IF EXISTS trigger_manage_service_verification_timestamps ON "Service";
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_manage_service_visibility_timestamps
|
||||||
|
BEFORE UPDATE OF "serviceVisibility" ON "Service"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION manage_service_visibility_timestamps();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_manage_service_verification_timestamps
|
||||||
BEFORE UPDATE OF "verificationStatus" ON "Service"
|
BEFORE UPDATE OF "verificationStatus" ON "Service"
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
-- Only execute the function if the verificationStatus value has actually changed
|
EXECUTE FUNCTION manage_service_verification_timestamps();
|
||||||
WHEN (OLD."verificationStatus" IS DISTINCT FROM NEW."verificationStatus")
|
|
||||||
EXECUTE FUNCTION manage_service_timestamps();
|
|
||||||
|
|||||||
@@ -65,13 +65,13 @@ export const apiServiceActions = {
|
|||||||
tosUrls: true,
|
tosUrls: true,
|
||||||
referral: true,
|
referral: true,
|
||||||
listedAt: true,
|
listedAt: true,
|
||||||
|
approvedAt: true,
|
||||||
verifiedAt: true,
|
verifiedAt: true,
|
||||||
serviceVisibility: true,
|
serviceVisibility: true,
|
||||||
} as const satisfies Prisma.ServiceSelect
|
} as const satisfies Prisma.ServiceSelect
|
||||||
|
|
||||||
let service = await prisma.service.findFirst({
|
let service = await prisma.service.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
|
|
||||||
OR: [
|
OR: [
|
||||||
@@ -92,7 +92,6 @@ export const apiServiceActions = {
|
|||||||
if (!service && input.slug) {
|
if (!service && input.slug) {
|
||||||
service = await prisma.service.findFirst({
|
service = await prisma.service.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
|
|
||||||
previousSlugs: { has: input.slug },
|
previousSlugs: { has: input.slug },
|
||||||
@@ -105,9 +104,7 @@ export const apiServiceActions = {
|
|||||||
!service ||
|
!service ||
|
||||||
(service.serviceVisibility !== 'PUBLIC' &&
|
(service.serviceVisibility !== 'PUBLIC' &&
|
||||||
service.serviceVisibility !== 'ARCHIVED' &&
|
service.serviceVisibility !== 'ARCHIVED' &&
|
||||||
service.serviceVisibility !== 'UNLISTED') ||
|
service.serviceVisibility !== 'UNLISTED')
|
||||||
!service.listedAt ||
|
|
||||||
service.listedAt > new Date()
|
|
||||||
) {
|
) {
|
||||||
throw new ActionError({
|
throw new ActionError({
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
@@ -130,6 +127,7 @@ export const apiServiceActions = {
|
|||||||
'description',
|
'description',
|
||||||
]),
|
]),
|
||||||
verifiedAt: service.verifiedAt,
|
verifiedAt: service.verifiedAt,
|
||||||
|
approvedAt: service.approvedAt,
|
||||||
kycLevel: service.kycLevel,
|
kycLevel: service.kycLevel,
|
||||||
kycLevelInfo: pick(getKycLevelInfo(service.kycLevel.toString()), ['value', 'name', 'description']),
|
kycLevelInfo: pick(getKycLevelInfo(service.kycLevel.toString()), ['value', 'name', 'description']),
|
||||||
kycLevelClarification: service.kycLevelClarification,
|
kycLevelClarification: service.kycLevelClarification,
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const findPossibleDuplicates = async (input: { name: string }) => {
|
|||||||
id: {
|
id: {
|
||||||
in: matches.map(({ id }) => id),
|
in: matches.map(({ id }) => id),
|
||||||
},
|
},
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: {
|
serviceVisibility: {
|
||||||
in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'],
|
in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'],
|
||||||
},
|
},
|
||||||
@@ -252,7 +251,6 @@ export const serviceSuggestionActions = {
|
|||||||
overallScore: 0,
|
overallScore: 0,
|
||||||
privacyScore: 0,
|
privacyScore: 0,
|
||||||
trustScore: 0,
|
trustScore: 0,
|
||||||
listedAt: new Date(),
|
|
||||||
serviceVisibility: 'UNLISTED',
|
serviceVisibility: 'UNLISTED',
|
||||||
categories: {
|
categories: {
|
||||||
connect: input.categories.map((id) => ({ id })),
|
connect: input.categories.map((id) => ({ id })),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { pwaAssetsHead } from 'virtual:pwa-assets/head'
|
|||||||
import { pwaInfo } from 'virtual:pwa-info'
|
import { pwaInfo } from 'virtual:pwa-info'
|
||||||
|
|
||||||
import { isNotArray } from '../lib/arrays'
|
import { isNotArray } from '../lib/arrays'
|
||||||
import { DEPLOYMENT_MODE } from '../lib/envVariables'
|
import { DEPLOYMENT_MODE } from '../lib/client/envVariables'
|
||||||
|
|
||||||
import DevToolsMessageScript from './DevToolsMessageScript.astro'
|
import DevToolsMessageScript from './DevToolsMessageScript.astro'
|
||||||
import DynamicFavicon from './DynamicFavicon.astro'
|
import DynamicFavicon from './DynamicFavicon.astro'
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '../lib/commentsWithReplies'
|
} from '../lib/commentsWithReplies'
|
||||||
import { computeKarmaUnlocks } from '../lib/karmaUnlocks'
|
import { computeKarmaUnlocks } from '../lib/karmaUnlocks'
|
||||||
import { formatDateShort } from '../lib/timeAgo'
|
import { formatDateShort } from '../lib/timeAgo'
|
||||||
|
import { urlDomain } from '../lib/urls'
|
||||||
|
|
||||||
import BadgeSmall from './BadgeSmall.astro'
|
import BadgeSmall from './BadgeSmall.astro'
|
||||||
import CommentModeration from './CommentModeration.astro'
|
import CommentModeration from './CommentModeration.astro'
|
||||||
@@ -170,7 +171,7 @@ const commentUrl = makeCommentUrl({ serviceSlug, commentId: comment.id, origin:
|
|||||||
comment.author.admin || comment.author.moderator
|
comment.author.admin || comment.author.moderator
|
||||||
? `KYCnot.me ${comment.author.admin ? 'Admin' : 'Moderator'}${comment.author.verifiedLink ? '. ' : ''}`
|
? `KYCnot.me ${comment.author.admin ? 'Admin' : 'Moderator'}${comment.author.verifiedLink ? '. ' : ''}`
|
||||||
: ''
|
: ''
|
||||||
}${comment.author.verifiedLink ? `Related to ${comment.author.verifiedLink}` : ''}`}
|
}${comment.author.verifiedLink ? `Related to ${urlDomain(comment.author.verifiedLink)}` : ''}`}
|
||||||
>
|
>
|
||||||
<Icon name="ri:verified-badge-fill" class="size-4 text-cyan-300" />
|
<Icon name="ri:verified-badge-fill" class="size-4 text-cyan-300" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import { DEPLOYMENT_MODE } from '../lib/envVariables'
|
import { DEPLOYMENT_MODE } from '../lib/client/envVariables'
|
||||||
import { prisma } from '../lib/prisma'
|
import { prisma } from '../lib/prisma'
|
||||||
|
|
||||||
const user = Astro.locals.user
|
const user = Astro.locals.user
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Icon } from 'astro-icon/components'
|
|||||||
import { sample } from 'lodash-es'
|
import { sample } from 'lodash-es'
|
||||||
|
|
||||||
import { splashTexts } from '../constants/splashTexts'
|
import { splashTexts } from '../constants/splashTexts'
|
||||||
|
import { DEPLOYMENT_MODE } from '../lib/client/envVariables'
|
||||||
import { cn } from '../lib/cn'
|
import { cn } from '../lib/cn'
|
||||||
import { DEPLOYMENT_MODE } from '../lib/envVariables'
|
|
||||||
import { makeLoginUrl, makeUnimpersonateUrl } from '../lib/redirectUrls'
|
import { makeLoginUrl, makeUnimpersonateUrl } from '../lib/redirectUrls'
|
||||||
|
|
||||||
import AdminOnly from './AdminOnly.astro'
|
import AdminOnly from './AdminOnly.astro'
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type Props<Multiple extends boolean = false> = Omit<
|
|||||||
iconClass?: string
|
iconClass?: string
|
||||||
description?: MarkdownString
|
description?: MarkdownString
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
noTransitionPersist?: boolean
|
|
||||||
}[]
|
}[]
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
selectedValue?: Multiple extends true ? string[] : string
|
selectedValue?: Multiple extends true ? string[] : string
|
||||||
@@ -70,7 +69,7 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
transition:persist={option.noTransitionPersist || !multiple ? undefined : true}
|
transition:persist
|
||||||
type={multiple ? 'checkbox' : 'radio'}
|
type={multiple ? 'checkbox' : 'radio'}
|
||||||
name={wrapperProps.name}
|
name={wrapperProps.name}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
|
|||||||
@@ -4,14 +4,17 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { isBrowserNotificationsEnabled, showBrowserNotification } from '../lib/client/browserNotifications'
|
import { isBrowserNotificationsEnabled, showBrowserNotification } from '../lib/client/browserNotifications'
|
||||||
import { makeNotificationOptions } from '../lib/notificationOptions'
|
import {
|
||||||
|
makeBrowserNotificationOptions,
|
||||||
|
makeBrowserNotificationTitle,
|
||||||
|
} from '../lib/client/notificationOptions'
|
||||||
|
|
||||||
document.addEventListener('sse:new-notification', (event) => {
|
document.addEventListener('sse:new-notification', (event) => {
|
||||||
if (isBrowserNotificationsEnabled()) {
|
if (isBrowserNotificationsEnabled()) {
|
||||||
const payload = event.detail
|
const payload = event.detail
|
||||||
const notification = showBrowserNotification(
|
const notification = showBrowserNotification(
|
||||||
payload.title,
|
makeBrowserNotificationTitle(payload.title),
|
||||||
makeNotificationOptions(payload, { removeActions: true })
|
makeBrowserNotificationOptions(payload, { removeActions: true })
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle notification click
|
// Handle notification click
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
---
|
---
|
||||||
import { Icon } from 'astro-icon/components'
|
import { Icon } from 'astro-icon/components'
|
||||||
import { differenceInDays, isPast } from 'date-fns'
|
import { differenceInDays } from 'date-fns'
|
||||||
|
|
||||||
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
||||||
import { verificationStepStatusesByValue } from '../constants/verificationStepStatus'
|
import { verificationStepStatusesByValue } from '../constants/verificationStepStatus'
|
||||||
import { cn } from '../lib/cn'
|
import { cn } from '../lib/cn'
|
||||||
|
|
||||||
import TimeFormatted from './TimeFormatted.astro'
|
|
||||||
|
|
||||||
import type { Prisma } from '@prisma/client'
|
import type { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
const RECENTLY_ADDED_DAYS = 7
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
service: Prisma.ServiceGetPayload<{
|
service: Prisma.ServiceGetPayload<{
|
||||||
select: {
|
select: {
|
||||||
verificationStatus: true
|
verificationStatus: true
|
||||||
verificationProofMd: true
|
verificationProofMd: true
|
||||||
verificationSummary: true
|
verificationSummary: true
|
||||||
listedAt: true
|
approvedAt: true
|
||||||
|
isRecentlyApproved: true
|
||||||
createdAt: true
|
createdAt: true
|
||||||
verificationSteps: {
|
verificationSteps: {
|
||||||
select: {
|
select: {
|
||||||
@@ -31,8 +28,14 @@ type Props = {
|
|||||||
|
|
||||||
const { service } = Astro.props
|
const { service } = Astro.props
|
||||||
|
|
||||||
const listedDate = service.listedAt ?? service.createdAt
|
function formatApprovedAt(approvedAt: Date | null) {
|
||||||
const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), listedDate) < RECENTLY_ADDED_DAYS
|
if (!approvedAt) return 'less than 15 days ago'
|
||||||
|
|
||||||
|
const days = differenceInDays(new Date(), approvedAt)
|
||||||
|
if (days === 0) return 'today'
|
||||||
|
if (days === 1) return 'yesterday'
|
||||||
|
return `${days.toLocaleString()} days ago`
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -67,10 +70,10 @@ const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), list
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : wasRecentlyAdded ? (
|
) : service.isRecentlyApproved ? (
|
||||||
<div class="mb-3 rounded-md bg-yellow-900/50 p-2 text-sm text-yellow-400">
|
<div class="mb-3 rounded-md bg-yellow-900/50 p-2 text-sm text-yellow-400">
|
||||||
This service was {service.listedAt === null ? 'added ' : 'listed '}{' '}
|
This service was approved
|
||||||
<TimeFormatted date={listedDate} daysUntilDate={RECENTLY_ADDED_DAYS} />
|
{formatApprovedAt(service.approvedAt)}
|
||||||
{service.verificationStatus !== 'VERIFICATION_SUCCESS' && ' and it is not verified'}. Proceed with
|
{service.verificationStatus !== 'VERIFICATION_SUCCESS' && ' and it is not verified'}. Proceed with
|
||||||
caution.
|
caution.
|
||||||
<a
|
<a
|
||||||
@@ -87,7 +90,7 @@ const wasRecentlyAdded = isPast(listedDate) && differenceInDays(new Date(), list
|
|||||||
Basic checks passed, but not fully verified.
|
Basic checks passed, but not fully verified.
|
||||||
<a
|
<a
|
||||||
href="/about#suggestion-review-process"
|
href="/about#suggestion-review-process"
|
||||||
class="text-yellow-100 underline opacity-50 transition-opacity hover:opacity-100 focus-visible:opacity-100"
|
class="text-blue-100 underline opacity-50 transition-opacity hover:opacity-100 focus-visible:opacity-100"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ type NonDbAttributeFull = NonDbAttribute & {
|
|||||||
select: {
|
select: {
|
||||||
verificationStatus: true
|
verificationStatus: true
|
||||||
serviceVisibility: true
|
serviceVisibility: true
|
||||||
isRecentlyListed: true
|
isRecentlyApproved: true
|
||||||
listedAt: true
|
approvedAt: true
|
||||||
createdAt: true
|
createdAt: true
|
||||||
tosReviewAt: true
|
tosReviewAt: true
|
||||||
tosReview: true
|
tosReview: true
|
||||||
@@ -189,17 +189,17 @@ export const nonDbAttributes: NonDbAttributeFull[] = [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'recently-listed',
|
slug: 'recently-approved',
|
||||||
title: 'Recently listed',
|
title: 'Recently approved',
|
||||||
type: 'WARNING',
|
type: 'WARNING',
|
||||||
category: 'TRUST',
|
category: 'TRUST',
|
||||||
description: 'Listed on KYCnot.me less than 15 days ago. Proceed with caution.',
|
description: 'Approved on KYCnot.me less than 15 days ago. Proceed with caution.',
|
||||||
privacyPoints: 0,
|
privacyPoints: 0,
|
||||||
trustPoints: -5,
|
trustPoints: -5,
|
||||||
links: [],
|
links: [],
|
||||||
customize: (service) => ({
|
customize: (service) => ({
|
||||||
show: service.isRecentlyListed,
|
show: service.isRecentlyApproved,
|
||||||
description: `Listed on KYCnot.me ${formatDateShort(service.listedAt ?? service.createdAt)}. Proceed with caution.`,
|
description: `Approved on KYCnot.me ${formatDateShort(service.approvedAt ?? service.createdAt)}. Proceed with caution.`,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -217,41 +217,22 @@ export const nonDbAttributes: NonDbAttributeFull[] = [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: 'has-onion-urls',
|
slug: 'has-onion-or-i2p-urls',
|
||||||
title: 'Has Onion URLs',
|
title: 'Has Onion or I2P URLs',
|
||||||
type: 'GOOD',
|
type: 'GOOD',
|
||||||
category: 'PRIVACY',
|
category: 'PRIVACY',
|
||||||
description: 'Onion (Tor) URLs enhance privacy and anonymity.',
|
description: 'Onion (Tor) and I2P URLs enhance privacy and anonymity.',
|
||||||
privacyPoints: 5,
|
privacyPoints: 5,
|
||||||
trustPoints: 0,
|
trustPoints: 0,
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
url: '/?onion=true',
|
url: '/?networks=onion&networks=i2p',
|
||||||
label: 'Search with this',
|
label: 'Search with this',
|
||||||
icon: 'ri:search-line',
|
icon: 'ri:search-line',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
customize: (service) => ({
|
customize: (service) => ({
|
||||||
show: service.onionUrls.length > 0,
|
show: service.onionUrls.length > 0 || service.i2pUrls.length > 0,
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'has-i2p-urls',
|
|
||||||
title: 'Has I2P URLs',
|
|
||||||
type: 'GOOD',
|
|
||||||
category: 'PRIVACY',
|
|
||||||
description: 'I2P URLs enhance privacy and anonymity.',
|
|
||||||
privacyPoints: 5,
|
|
||||||
trustPoints: 0,
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
url: '/?i2p=true',
|
|
||||||
label: 'Search with this',
|
|
||||||
icon: 'ri:search-line',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
customize: (service) => ({
|
|
||||||
show: service.i2pUrls.length > 0,
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
7
web/src/lib/client/envVariables.ts
Normal file
7
web/src/lib/client/envVariables.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const DEPLOYMENT_MODE = import.meta.env.PROD
|
||||||
|
? import.meta.env.MODE === 'development' ||
|
||||||
|
import.meta.env.MODE === 'staging' ||
|
||||||
|
import.meta.env.MODE === 'production'
|
||||||
|
? import.meta.env.MODE
|
||||||
|
: 'development'
|
||||||
|
: 'development'
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { NotificationData, NotificationPayload } from './serverEventsTypes'
|
import { DEPLOYMENT_MODE } from './envVariables'
|
||||||
|
|
||||||
|
import type { NotificationData, NotificationPayload } from '../serverEventsTypes'
|
||||||
|
|
||||||
export type CustomNotificationOptions = NotificationOptions & {
|
export type CustomNotificationOptions = NotificationOptions & {
|
||||||
actions?: { action: string; title: string; icon?: string }[]
|
actions?: { action: string; title: string; icon?: string }[]
|
||||||
@@ -6,14 +8,24 @@ export type CustomNotificationOptions = NotificationOptions & {
|
|||||||
data: NotificationData
|
data: NotificationData
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeNotificationOptions(
|
export function makeBrowserNotificationTitle(title?: string | null) {
|
||||||
|
const prefix = DEPLOYMENT_MODE === 'development' ? '[DEV] ' : DEPLOYMENT_MODE === 'staging' ? '[PRE] ' : ''
|
||||||
|
return `${prefix}${title ?? 'New Notification'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeBrowserNotificationOptions(
|
||||||
payload: NotificationPayload | null,
|
payload: NotificationPayload | null,
|
||||||
options: { removeActions?: boolean } = {}
|
options: { removeActions?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
const defaultOptions: CustomNotificationOptions = {
|
const defaultOptions: CustomNotificationOptions = {
|
||||||
body: 'You have a new notification',
|
body: 'You have a new notification',
|
||||||
lang: 'en-US',
|
lang: 'en-US',
|
||||||
icon: '/favicon.svg',
|
icon:
|
||||||
|
DEPLOYMENT_MODE === 'development'
|
||||||
|
? '/favicon-dev.svg'
|
||||||
|
: DEPLOYMENT_MODE === 'staging'
|
||||||
|
? '/favicon-stage.svg'
|
||||||
|
: '/favicon.svg',
|
||||||
badge: '/notification-icon.svg',
|
badge: '/notification-icon.svg',
|
||||||
requireInteraction: false,
|
requireInteraction: false,
|
||||||
silent: false,
|
silent: false,
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { z } from 'astro/zod'
|
|
||||||
|
|
||||||
const schema = z.enum(['development', 'staging', 'production'])
|
|
||||||
|
|
||||||
export const DEPLOYMENT_MODE = schema.parse(import.meta.env.PROD ? import.meta.env.MODE : 'development')
|
|
||||||
@@ -39,7 +39,6 @@ export async function getService(slug: string | undefined): Promise<
|
|||||||
const service =
|
const service =
|
||||||
(await prisma.service.findFirst({
|
(await prisma.service.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
slug,
|
slug,
|
||||||
},
|
},
|
||||||
@@ -47,7 +46,6 @@ export async function getService(slug: string | undefined): Promise<
|
|||||||
})) ??
|
})) ??
|
||||||
(await prisma.service.findFirst({
|
(await prisma.service.findFirst({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'] },
|
||||||
previousSlugs: { has: slug },
|
previousSlugs: { has: slug },
|
||||||
},
|
},
|
||||||
@@ -175,7 +173,6 @@ export async function getEvents(): Promise<
|
|||||||
where: {
|
where: {
|
||||||
visible: true,
|
visible: true,
|
||||||
service: {
|
service: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '../constants/characters'
|
} from '../constants/characters'
|
||||||
|
|
||||||
import { getRandom, typedJoin } from './arrays'
|
import { getRandom, typedJoin } from './arrays'
|
||||||
import { DEPLOYMENT_MODE } from './envVariables'
|
import { DEPLOYMENT_MODE } from './client/envVariables'
|
||||||
import { transformCase } from './strings'
|
import { transformCase } from './strings'
|
||||||
|
|
||||||
const DIGEST = 'sha512'
|
const DIGEST = 'sha512'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { LOGS_UI_URL } from 'astro:env/server'
|
|||||||
|
|
||||||
import { SUPPORT_EMAIL } from '../constants/project'
|
import { SUPPORT_EMAIL } from '../constants/project'
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||||
import { DEPLOYMENT_MODE } from '../lib/envVariables'
|
import { DEPLOYMENT_MODE } from '../lib/client/envVariables'
|
||||||
import { zodParseQueryParamsStoringErrors } from '../lib/parseUrlFilters'
|
import { zodParseQueryParamsStoringErrors } from '../lib/parseUrlFilters'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ The trust score represents how reliable and trustworthy a service is, based on o
|
|||||||
|
|
||||||
##### Overall Score
|
##### Overall Score
|
||||||
|
|
||||||
The overall score is calculated as `(privacy * 0.6) + (trust * 0.4)` and provides a combined measure of privacy and trust.
|
The overall score is calculated as `(privacy * 0.6) + (trust * 0.4)` truncated. This provides a combined measure of privacy and trust.
|
||||||
|
|
||||||
#### Terms of Service Reviews
|
#### Terms of Service Reviews
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { makeUserWithKarmaUnlocks } from '../../lib/karmaUnlocks'
|
|||||||
import { prisma } from '../../lib/prisma'
|
import { prisma } from '../../lib/prisma'
|
||||||
import { makeLoginUrl } from '../../lib/redirectUrls'
|
import { makeLoginUrl } from '../../lib/redirectUrls'
|
||||||
import { formatDateShort } from '../../lib/timeAgo'
|
import { formatDateShort } from '../../lib/timeAgo'
|
||||||
|
import { urlDomain } from '../../lib/urls'
|
||||||
|
|
||||||
const userId = Astro.locals.user?.id
|
const userId = Astro.locals.user?.id
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -423,7 +424,7 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="text-blue-400 hover:underline"
|
class="text-blue-400 hover:underline"
|
||||||
>
|
>
|
||||||
{user.verifiedLink}
|
{urlDomain(user.verifiedLink)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -857,12 +858,7 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-center">
|
<td class="px-4 py-3 text-center">
|
||||||
<span
|
<span class="border-night-500/20 bg-night-800/10 inline-flex items-center rounded-full border px-2 py-0.5 text-xs">
|
||||||
class={cn(
|
|
||||||
'border-night-500/20 bg-night-800/10 inline-flex items-center rounded-full border px-2 py-0.5 text-xs',
|
|
||||||
statusInfo.iconClass
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon name={statusInfo.icon} class="mr-1 size-3" />
|
<Icon name={statusInfo.icon} class="mr-1 size-3" />
|
||||||
{statusInfo.label}
|
{statusInfo.label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -271,7 +271,6 @@ if (toggleResult?.error) {
|
|||||||
label: type.label,
|
label: type.label,
|
||||||
value: type.value,
|
value: type.value,
|
||||||
icon: type.icon,
|
icon: type.icon,
|
||||||
noTransitionPersist: false,
|
|
||||||
}))}
|
}))}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
required
|
required
|
||||||
@@ -306,8 +305,8 @@ if (toggleResult?.error) {
|
|||||||
label="Status"
|
label="Status"
|
||||||
error={createInputErrors.isActive}
|
error={createInputErrors.isActive}
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Active', value: 'true', noTransitionPersist: true },
|
{ label: 'Active', value: 'true' },
|
||||||
{ label: 'Inactive', value: 'false', noTransitionPersist: true },
|
{ label: 'Inactive', value: 'false' },
|
||||||
]}
|
]}
|
||||||
selectedValue={newAnnouncement.isActive ? 'true' : 'false'}
|
selectedValue={newAnnouncement.isActive ? 'true' : 'false'}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
@@ -628,7 +627,6 @@ if (toggleResult?.error) {
|
|||||||
label: type.label,
|
label: type.label,
|
||||||
value: type.value,
|
value: type.value,
|
||||||
icon: type.icon,
|
icon: type.icon,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
required
|
required
|
||||||
@@ -661,8 +659,8 @@ if (toggleResult?.error) {
|
|||||||
name="isActive"
|
name="isActive"
|
||||||
label="Status"
|
label="Status"
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Active', value: 'true', noTransitionPersist: true },
|
{ label: 'Active', value: 'true' },
|
||||||
{ label: 'Inactive', value: 'false', noTransitionPersist: true },
|
{ label: 'Inactive', value: 'false' },
|
||||||
]}
|
]}
|
||||||
selectedValue={announcement.isActive ? 'true' : 'false'}
|
selectedValue={announcement.isActive ? 'true' : 'false'}
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
verificationStepStatuses,
|
verificationStepStatuses,
|
||||||
} from '../../../../constants/verificationStepStatus'
|
} from '../../../../constants/verificationStepStatus'
|
||||||
import BaseLayout from '../../../../layouts/BaseLayout.astro'
|
import BaseLayout from '../../../../layouts/BaseLayout.astro'
|
||||||
import { DEPLOYMENT_MODE } from '../../../../lib/envVariables'
|
import { DEPLOYMENT_MODE } from '../../../../lib/client/envVariables'
|
||||||
import { listFiles } from '../../../../lib/fileStorage'
|
import { listFiles } from '../../../../lib/fileStorage'
|
||||||
import { makeAdminApiCallInfo } from '../../../../lib/makeAdminApiCallInfo'
|
import { makeAdminApiCallInfo } from '../../../../lib/makeAdminApiCallInfo'
|
||||||
import { pluralize } from '../../../../lib/pluralize'
|
import { pluralize } from '../../../../lib/pluralize'
|
||||||
@@ -441,7 +441,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
value: kycLevel.id.toString(),
|
value: kycLevel.id.toString(),
|
||||||
icon: kycLevel.icon,
|
icon: kycLevel.icon,
|
||||||
description: kycLevel.description,
|
description: kycLevel.description,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.kycLevel.toString()}
|
selectedValue={service.kycLevel.toString()}
|
||||||
iconSize="md"
|
iconSize="md"
|
||||||
@@ -458,7 +457,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
value: clarification.value,
|
value: clarification.value,
|
||||||
icon: clarification.icon,
|
icon: clarification.icon,
|
||||||
description: clarification.description,
|
description: clarification.description,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.kycLevelClarification}
|
selectedValue={service.kycLevelClarification}
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
@@ -475,7 +473,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
icon: status.icon,
|
icon: status.icon,
|
||||||
iconClass: status.classNames.icon,
|
iconClass: status.classNames.icon,
|
||||||
description: status.description,
|
description: status.description,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.verificationStatus}
|
selectedValue={service.verificationStatus}
|
||||||
error={serviceInputErrors.verificationStatus}
|
error={serviceInputErrors.verificationStatus}
|
||||||
@@ -491,7 +488,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
label: currency.name,
|
label: currency.name,
|
||||||
value: currency.id,
|
value: currency.id,
|
||||||
icon: currency.icon,
|
icon: currency.icon,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.acceptedCurrencies}
|
selectedValue={service.acceptedCurrencies}
|
||||||
error={serviceInputErrors.acceptedCurrencies}
|
error={serviceInputErrors.acceptedCurrencies}
|
||||||
@@ -532,7 +528,6 @@ const apiCalls = await Astro.locals.banners.try(
|
|||||||
icon: visibility.icon,
|
icon: visibility.icon,
|
||||||
iconClass: visibility.iconClass,
|
iconClass: visibility.iconClass,
|
||||||
description: visibility.description,
|
description: visibility.description,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
selectedValue={service.serviceVisibility}
|
selectedValue={service.serviceVisibility}
|
||||||
error={serviceInputErrors.serviceVisibility}
|
error={serviceInputErrors.serviceVisibility}
|
||||||
|
|||||||
@@ -230,26 +230,22 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
label: 'Admin',
|
label: 'Admin',
|
||||||
value: 'admin',
|
value: 'admin',
|
||||||
icon: 'ri:shield-star-fill',
|
icon: 'ri:shield-star-fill',
|
||||||
noTransitionPersist: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Moderator',
|
label: 'Moderator',
|
||||||
value: 'moderator',
|
value: 'moderator',
|
||||||
icon: 'ri:graduation-cap-fill',
|
icon: 'ri:graduation-cap-fill',
|
||||||
noTransitionPersist: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Spammer',
|
label: 'Spammer',
|
||||||
value: 'spammer',
|
value: 'spammer',
|
||||||
icon: 'ri:alert-fill',
|
icon: 'ri:alert-fill',
|
||||||
noTransitionPersist: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Verified',
|
label: 'Verified',
|
||||||
value: 'verified',
|
value: 'verified',
|
||||||
icon: 'ri:verified-badge-fill',
|
icon: 'ri:verified-badge-fill',
|
||||||
disabled: true,
|
disabled: true,
|
||||||
noTransitionPersist: true,
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
selectedValue={[
|
selectedValue={[
|
||||||
@@ -434,7 +430,6 @@ if (!user) return Astro.rewrite('/404')
|
|||||||
label: role.label,
|
label: role.label,
|
||||||
value: role.value,
|
value: role.value,
|
||||||
icon: role.icon,
|
icon: role.icon,
|
||||||
noTransitionPersist: true,
|
|
||||||
}))}
|
}))}
|
||||||
required
|
required
|
||||||
cardSize="sm"
|
cardSize="sm"
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ type ServiceResponse = {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
verifiedAt: Date | null
|
verifiedAt: Date | null
|
||||||
|
approvedAt: Date | null
|
||||||
kycLevel: 0 | 1 | 2 | 3 | 4
|
kycLevel: 0 | 1 | 2 | 3 | 4
|
||||||
kycLevelInfo: {
|
kycLevelInfo: {
|
||||||
value: 0 | 1 | 2 | 3 | 4
|
value: 0 | 1 | 2 | 3 | 4
|
||||||
@@ -145,7 +146,8 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \
|
|||||||
"labelShort": "Verified",
|
"labelShort": "Verified",
|
||||||
"description": "Thoroughly tested and verified by the team. But things might change, this is not a guarantee."
|
"description": "Thoroughly tested and verified by the team. But things might change, this is not a guarantee."
|
||||||
},
|
},
|
||||||
"verifiedAt": "2025-01-20T07:12:29.393Z",
|
"verifiedAt": "2025-06-14T11:02:39.294Z",
|
||||||
|
"approvedAt": "2025-05-31T19:09:18.043Z",
|
||||||
"kycLevel": 0,
|
"kycLevel": 0,
|
||||||
"kycLevelInfo": {
|
"kycLevelInfo": {
|
||||||
"value": 0,
|
"value": 0,
|
||||||
@@ -163,7 +165,7 @@ curl -X QUERY https://kycnot.me/api/v1/service/get \
|
|||||||
"slug": "exchange"
|
"slug": "exchange"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"listedAt": "2025-05-31T19:09:18.043Z",
|
"listedAt": "2025-04-20T07:12:29.393Z",
|
||||||
"serviceUrls": [
|
"serviceUrls": [
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
"http://c9ikae0fdidzh1ufrzp022e5uqfvz6ofxlkycz59cvo6fdxjgx7ekl9e.onion"
|
"http://c9ikae0fdidzh1ufrzp022e5uqfvz6ofxlkycz59cvo6fdxjgx7ekl9e.onion"
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ const [services, [dbEvents, totalEvents]] = await Astro.locals.banners.tryMany([
|
|||||||
async () =>
|
async () =>
|
||||||
prisma.service.findMany({
|
prisma.service.findMany({
|
||||||
where: {
|
where: {
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -72,7 +71,6 @@ const [services, [dbEvents, totalEvents]] = await Astro.locals.banners.tryMany([
|
|||||||
},
|
},
|
||||||
service: {
|
service: {
|
||||||
slug: params.service ?? undefined,
|
slug: params.service ?? undefined,
|
||||||
listedAt: params.service ? undefined : { lte: new Date() },
|
|
||||||
serviceVisibility: {
|
serviceVisibility: {
|
||||||
in: params.service ? ['PUBLIC', 'ARCHIVED', 'UNLISTED'] : ['PUBLIC', 'ARCHIVED'],
|
in: params.service ? ['PUBLIC', 'ARCHIVED', 'UNLISTED'] : ['PUBLIC', 'ARCHIVED'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ const servicesQMatch = filters.q ? await findServicesBySimilarity(filters.q) : n
|
|||||||
|
|
||||||
const where = {
|
const where = {
|
||||||
id: servicesQMatch ? { in: servicesQMatch.map(({ id }) => id) } : undefined,
|
id: servicesQMatch ? { in: servicesQMatch.map(({ id }) => id) } : undefined,
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
categories: filters.categories.length ? { some: { slug: { in: filters.categories } } } : undefined,
|
categories: filters.categories.length ? { some: { slug: { in: filters.categories } } } : undefined,
|
||||||
verificationStatus: {
|
verificationStatus: {
|
||||||
in: includeScams ? uniq([...filters.verification, 'VERIFICATION_FAILED'] as const) : filters.verification,
|
in: includeScams ? uniq([...filters.verification, 'VERIFICATION_FAILED'] as const) : filters.verification,
|
||||||
@@ -317,7 +316,6 @@ const [categories, [services, totalServices], countCommunityOnly, attributes] =
|
|||||||
services: {
|
services: {
|
||||||
where: {
|
where: {
|
||||||
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'ARCHIVED'] },
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
|
|||||||
where: {
|
where: {
|
||||||
slug,
|
slug,
|
||||||
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -93,6 +92,7 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
|
|||||||
referral: true,
|
referral: true,
|
||||||
imageUrl: true,
|
imageUrl: true,
|
||||||
listedAt: true,
|
listedAt: true,
|
||||||
|
approvedAt: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
acceptedCurrencies: true,
|
acceptedCurrencies: true,
|
||||||
tosReview: true,
|
tosReview: true,
|
||||||
@@ -100,7 +100,7 @@ const [service, dbNotificationPreferences] = await Astro.locals.banners.tryMany(
|
|||||||
userSentiment: true,
|
userSentiment: true,
|
||||||
userSentimentAt: true,
|
userSentimentAt: true,
|
||||||
averageUserRating: true,
|
averageUserRating: true,
|
||||||
isRecentlyListed: true,
|
isRecentlyApproved: true,
|
||||||
contactMethods: {
|
contactMethods: {
|
||||||
select: {
|
select: {
|
||||||
value: true,
|
value: true,
|
||||||
@@ -230,7 +230,6 @@ if (!service) {
|
|||||||
where: {
|
where: {
|
||||||
previousSlugs: { has: slug },
|
previousSlugs: { has: slug },
|
||||||
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
serviceVisibility: { in: ['PUBLIC', 'UNLISTED', 'ARCHIVED'] },
|
||||||
listedAt: { lte: new Date() },
|
|
||||||
},
|
},
|
||||||
select: { slug: true },
|
select: { slug: true },
|
||||||
})
|
})
|
||||||
@@ -1005,15 +1004,23 @@ const activeEventToShow =
|
|||||||
attribute.typeInfo.classNames.text
|
attribute.typeInfo.classNames.text
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<span class="mr-auto ml-1">{attribute.title}</span>
|
||||||
name={attribute.categoryInfo.icon}
|
<>
|
||||||
class={cn('mr-2 size-4 flex-shrink-0', attribute.typeInfo.classNames.icon)}
|
{weights.map((w) => (
|
||||||
/>
|
<span
|
||||||
<span>{attribute.title}</span>
|
class={cn(
|
||||||
|
'ml-2 text-center text-sm',
|
||||||
|
w.value === 0 ? 'text-current/30' : 'text-current/60'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatNumber(w.value, w.formatOptions)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
<Icon
|
<Icon
|
||||||
name="ri:arrow-down-s-line"
|
name="ri:arrow-down-s-line"
|
||||||
class={cn(
|
class={cn(
|
||||||
'ml-auto size-5 group-open/attribute:rotate-180',
|
'ml-1 size-5 group-open/attribute:rotate-180',
|
||||||
attribute.typeInfo.classNames.icon
|
attribute.typeInfo.classNames.icon
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -1080,19 +1087,16 @@ const activeEventToShow =
|
|||||||
baseScoreType.classNames.text
|
baseScoreType.classNames.text
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<span class="font-title mr-auto ml-1">{baseScoreType.label}</span>
|
||||||
name={baseScoreType.icon}
|
<span class="mr-2 text-current/60">+50</span>
|
||||||
class={cn('mr-2 size-4 flex-shrink-0', baseScoreType.classNames.icon)}
|
<span class="mr-1 text-current/60">+50</span>
|
||||||
/>
|
|
||||||
<span class="font-title">{baseScoreType.label}</span>
|
|
||||||
<span class={cn('mr-1 ml-auto', baseScoreType.classNames.icon)}>+50</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<p class="text-day-400 mt-3 text-center text-xs">
|
<p class="text-day-400 mt-3 text-center text-xs">
|
||||||
<span class="hover:text-day-200 transition-colors">Overall = 60% Privacy + 40% Trust (Rounded)</span>
|
<span class="hover:text-day-200 transition-colors">Overall = 60% Privacy + 40% Trust (Truncated)</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="xs:gap-x-6 mt-2 flex flex-wrap justify-center gap-x-4 gap-y-2 text-xs">
|
<div class="xs:gap-x-6 mt-2 flex flex-wrap justify-center gap-x-4 gap-y-2 text-xs">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { makeUserWithKarmaUnlocks } from '../../lib/karmaUnlocks'
|
|||||||
import { prisma } from '../../lib/prisma'
|
import { prisma } from '../../lib/prisma'
|
||||||
import { KYCNOTME_SCHEMA_MINI } from '../../lib/schema'
|
import { KYCNOTME_SCHEMA_MINI } from '../../lib/schema'
|
||||||
import { formatDateShort } from '../../lib/timeAgo'
|
import { formatDateShort } from '../../lib/timeAgo'
|
||||||
|
import { urlDomain } from '../../lib/urls'
|
||||||
|
|
||||||
import type { ProfilePage, WithContext } from 'schema-dts'
|
import type { ProfilePage, WithContext } from 'schema-dts'
|
||||||
|
|
||||||
@@ -96,9 +97,6 @@ const user = await Astro.locals.banners.try('user', async () => {
|
|||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
service: {
|
service: {
|
||||||
listedAt: {
|
|
||||||
lte: new Date(),
|
|
||||||
},
|
|
||||||
serviceVisibility: {
|
serviceVisibility: {
|
||||||
in: ['PUBLIC', 'ARCHIVED'],
|
in: ['PUBLIC', 'ARCHIVED'],
|
||||||
},
|
},
|
||||||
@@ -511,7 +509,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="text-blue-400 hover:underline"
|
class="text-blue-400 hover:underline"
|
||||||
>
|
>
|
||||||
{user.verifiedLink}
|
{urlDomain(user.verifiedLink)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -975,12 +973,7 @@ const isCurrentUser = !!Astro.locals.user && user.id === Astro.locals.user.id
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-center">
|
<td class="px-4 py-3 text-center">
|
||||||
<span
|
<span class="border-night-500/20 bg-night-800/10 inline-flex items-center rounded-full border px-2 py-0.5 text-xs">
|
||||||
class={cn(
|
|
||||||
'border-night-500/20 bg-night-800/10 inline-flex items-center rounded-full border px-2 py-0.5 text-xs',
|
|
||||||
statusInfo.iconClass
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon name={statusInfo.icon} class="mr-1 size-3" />
|
<Icon name={statusInfo.icon} class="mr-1 size-3" />
|
||||||
{statusInfo.label}
|
{statusInfo.label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
import { clientsClaim } from 'workbox-core'
|
import { clientsClaim } from 'workbox-core'
|
||||||
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
|
||||||
|
|
||||||
import { makeNotificationOptions } from './lib/notificationOptions'
|
import {
|
||||||
|
makeBrowserNotificationOptions,
|
||||||
|
makeBrowserNotificationTitle,
|
||||||
|
} from './lib/client/notificationOptions'
|
||||||
|
|
||||||
import type { NotificationData, NotificationPayload } from './lib/serverEventsTypes'
|
import type { NotificationData, NotificationPayload } from './lib/serverEventsTypes'
|
||||||
|
|
||||||
@@ -59,8 +62,8 @@ async function handleNotificationClick(url: string) {
|
|||||||
|
|
||||||
async function showPushNotification(payload: NotificationPayload | null) {
|
async function showPushNotification(payload: NotificationPayload | null) {
|
||||||
await self.registration.showNotification(
|
await self.registration.showNotification(
|
||||||
payload?.title ?? 'New Notification',
|
makeBrowserNotificationTitle(payload?.title),
|
||||||
makeNotificationOptions(payload)
|
makeBrowserNotificationOptions(payload)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user