Compare commits
5 Commits
release-80
...
release-85
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ed07c8386 | ||
|
|
6a9f5f5e99 | ||
|
|
e6edee2dbe | ||
|
|
c7ee1606e4 | ||
|
|
f3c9b92ddb |
@@ -34,8 +34,8 @@ class TosReviewTask(Task):
|
||||
service_name = service["name"]
|
||||
verification_status = service.get("verificationStatus")
|
||||
|
||||
# Only process verified or approved services
|
||||
if verification_status not in ["VERIFICATION_SUCCESS", "APPROVED"]:
|
||||
# Only process verified, approved, or community contributed services
|
||||
if verification_status not in ["VERIFICATION_SUCCESS", "APPROVED", "COMMUNITY_CONTRIBUTED"]:
|
||||
self.logger.info(
|
||||
f"Skipping TOS review for service: {service_name} (ID: {service_id}) - Status: {verification_status}"
|
||||
)
|
||||
|
||||
48
web/package-lock.json
generated
48
web/package-lock.json
generated
@@ -2532,9 +2532,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2603,9 +2603,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7196,9 +7196,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9325,9 +9325,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9399,9 +9399,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9476,9 +9476,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10379,9 +10379,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -11671,9 +11671,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jake/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `userAgent` on the `PushSubscription` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "PushSubscription" DROP COLUMN "userAgent";
|
||||
@@ -687,8 +687,6 @@ model PushSubscription {
|
||||
p256dh String
|
||||
/// Authentication secret
|
||||
auth String
|
||||
/// To identify different devices
|
||||
userAgent String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
||||
@@ -270,6 +270,18 @@ export const commentActions = {
|
||||
}
|
||||
}
|
||||
|
||||
const isRelatedToService = !!(await tx.serviceUser.findUnique({
|
||||
where: {
|
||||
userId_serviceId: {
|
||||
userId: context.locals.user.id,
|
||||
serviceId: input.serviceId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
}))
|
||||
|
||||
// Prepare data object with proper type safety
|
||||
const commentData: Prisma.CommentCreateInput = {
|
||||
content: input.content,
|
||||
@@ -277,7 +289,12 @@ export const commentActions = {
|
||||
author: { connect: { id: context.locals.user.id } },
|
||||
|
||||
// Change status to HUMAN_PENDING if there's an issue report, this is so that the AI worker does not pick it up for review
|
||||
status: context.locals.user.admin ? 'APPROVED' : isIssueReport ? 'HUMAN_PENDING' : 'PENDING',
|
||||
status:
|
||||
context.locals.user.admin || context.locals.user.moderator || isRelatedToService
|
||||
? 'APPROVED'
|
||||
: isIssueReport
|
||||
? 'HUMAN_PENDING'
|
||||
: 'PENDING',
|
||||
requiresAdminReview,
|
||||
orderId: input.orderId?.trim() ?? null,
|
||||
kycRequested: input.issueKycRequested === true,
|
||||
|
||||
@@ -32,7 +32,6 @@ export const notificationActions = {
|
||||
endpoint: z.string(),
|
||||
p256dhKey: z.string(),
|
||||
authKey: z.string(),
|
||||
userAgent: z.string().optional(),
|
||||
}),
|
||||
handler: async (input, context) => {
|
||||
await prisma.pushSubscription.upsert({
|
||||
@@ -43,14 +42,12 @@ export const notificationActions = {
|
||||
update: {
|
||||
p256dh: input.p256dhKey,
|
||||
auth: input.authKey,
|
||||
userAgent: input.userAgent,
|
||||
},
|
||||
create: {
|
||||
userId: context.locals.user.id,
|
||||
endpoint: input.endpoint,
|
||||
p256dh: input.p256dhKey,
|
||||
auth: input.authKey,
|
||||
userAgent: input.userAgent,
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -58,25 +55,17 @@ export const notificationActions = {
|
||||
|
||||
unsubscribe: defineProtectedAction({
|
||||
accept: 'json',
|
||||
permissions: 'user',
|
||||
permissions: 'guest',
|
||||
input: z.object({
|
||||
endpoint: z.string().optional(),
|
||||
endpoint: z.string(),
|
||||
}),
|
||||
handler: async (input, context) => {
|
||||
if (input.endpoint) {
|
||||
await prisma.pushSubscription.deleteMany({
|
||||
where: {
|
||||
userId: context.locals.user.id,
|
||||
endpoint: input.endpoint,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await prisma.pushSubscription.deleteMany({
|
||||
where: {
|
||||
userId: context.locals.user.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
await prisma.pushSubscription.delete({
|
||||
where: {
|
||||
userId: context.locals.user?.id ?? undefined,
|
||||
endpoint: input.endpoint,
|
||||
},
|
||||
})
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -107,12 +107,8 @@ const ogImageUrl = makeOgImageUrl(ogImage, Astro.url)
|
||||
|
||||
<DynamicFavicon />
|
||||
|
||||
<!-- Components -->
|
||||
{
|
||||
!Astro.url.pathname.startsWith('/admin') && (
|
||||
<ClientRouter />
|
||||
) /* Disable to prevent bugs in important admin forms */
|
||||
}
|
||||
<ClientRouter />
|
||||
|
||||
<LoadingIndicator color="green" />
|
||||
<TailwindJsPluggin />
|
||||
{htmx && <HtmxScript />}
|
||||
|
||||
@@ -71,12 +71,13 @@ const [dbComments, pendingCommentsCount, activeRatingComment] = await Astro.loca
|
||||
'Failed to fetch comments',
|
||||
async () =>
|
||||
await prisma.comment.findMany(
|
||||
makeCommentsNestedQuery({
|
||||
await makeCommentsNestedQuery({
|
||||
depth: MAX_COMMENT_DEPTH,
|
||||
user,
|
||||
showPending: params.showPending,
|
||||
serviceId: service.id,
|
||||
sort: params.sort,
|
||||
highlightedCommentId: params.comment,
|
||||
})
|
||||
),
|
||||
[],
|
||||
|
||||
@@ -16,7 +16,6 @@ type Props = HTMLAttributes<'div'> & {
|
||||
pushSubscriptions: Prisma.PushSubscriptionGetPayload<{
|
||||
select: {
|
||||
endpoint: true
|
||||
userAgent: true
|
||||
}
|
||||
}>[]
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ if (!Astro.locals.user) return
|
||||
}
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
// NOTE: Disable sse: events when user is not logged in
|
||||
if (!document.body.hasAttribute('data-is-logged-in')) {
|
||||
stopServerEventsListener()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data as string)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<script>
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { unsubscribeFromPushNotifications } from '../lib/client/clientPushNotifications'
|
||||
|
||||
const NO_AUTO_RELOAD_ROUTES = ['/account/welcome', '/500', '/404'] as const satisfies `/${string}`[]
|
||||
|
||||
@@ -33,7 +34,7 @@
|
||||
|
||||
function shouldSkipAutoReload() {
|
||||
const currentPath = window.location.pathname
|
||||
const isErrorPage = document.querySelector('[data-is-error-page]') !== null
|
||||
const isErrorPage = document.body.hasAttribute('data-is-error-page')
|
||||
|
||||
return isErrorPage || NO_AUTO_RELOAD_ROUTES.some((route) => currentPath === route)
|
||||
}
|
||||
@@ -48,4 +49,11 @@
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
document.addEventListener('astro:page-load', async () => {
|
||||
if (!document.body.hasAttribute('data-is-logged-in')) {
|
||||
await unsubscribeFromPushNotifications()
|
||||
window.__SW_REGISTRATION__?.unregister()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { differenceInDays } from 'date-fns'
|
||||
|
||||
import { verificationStatusesByValue } from '../constants/verificationStatus'
|
||||
import { verificationStepStatusesByValue } from '../constants/verificationStepStatus'
|
||||
import { cn } from '../lib/cn'
|
||||
import { formatDaysAgo } from '../lib/timeAgo'
|
||||
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
@@ -27,15 +27,6 @@ type Props = {
|
||||
}
|
||||
|
||||
const { service } = Astro.props
|
||||
|
||||
function formatApprovedAt(approvedAt: Date | null) {
|
||||
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`
|
||||
}
|
||||
---
|
||||
|
||||
{
|
||||
@@ -61,9 +52,9 @@ function formatApprovedAt(approvedAt: Date | null) {
|
||||
<div class="mb-3 flex items-center gap-2 rounded-md bg-yellow-600/30 p-2 text-sm text-yellow-200">
|
||||
<Icon name="ri:alert-line" class="size-5 text-yellow-400" />
|
||||
<span>
|
||||
Community-contributed. Information not reviewed.
|
||||
Community contributed. Information not reviewed.
|
||||
<a
|
||||
href="/about#suggestion-review-process"
|
||||
href="/about#community-contributed"
|
||||
class="text-yellow-100 underline opacity-50 transition-opacity hover:opacity-100 focus-visible:opacity-100"
|
||||
>
|
||||
Learn more
|
||||
@@ -73,11 +64,11 @@ function formatApprovedAt(approvedAt: Date | null) {
|
||||
) : service.isRecentlyApproved ? (
|
||||
<div class="mb-3 rounded-md bg-yellow-900/50 p-2 text-sm text-yellow-400">
|
||||
This service was approved
|
||||
{formatApprovedAt(service.approvedAt)}
|
||||
{service.approvedAt ? formatDaysAgo(service.approvedAt) : 'less than 15 days ago'}
|
||||
{service.verificationStatus !== 'VERIFICATION_SUCCESS' && ' and it is not verified'}. Proceed with
|
||||
caution.
|
||||
<a
|
||||
href="/about#suggestion-review-process"
|
||||
href="/about#approved"
|
||||
class="text-yellow-100 underline opacity-50 transition-opacity hover:opacity-100 focus-visible:opacity-100"
|
||||
>
|
||||
Learn more
|
||||
@@ -87,9 +78,9 @@ function formatApprovedAt(approvedAt: Date | null) {
|
||||
<div class="mb-3 flex items-center gap-2 rounded-md bg-blue-600/30 p-2 text-sm text-blue-200">
|
||||
<Icon name="ri:information-line" class="size-5 text-blue-400" />
|
||||
<span>
|
||||
Basic checks passed, but not fully verified.
|
||||
Basic checks passed, but service is not verified.
|
||||
<a
|
||||
href="/about#suggestion-review-process"
|
||||
href="/about#approved"
|
||||
class="text-blue-100 underline opacity-50 transition-opacity hover:opacity-100 focus-visible:opacity-100"
|
||||
>
|
||||
Learn more
|
||||
|
||||
@@ -26,7 +26,7 @@ type VerificationStatusInfo<T extends string | null | undefined = string> = {
|
||||
}
|
||||
|
||||
export const READ_MORE_SENTENCE_LINK =
|
||||
'Read more about the [suggestion review process](/about#suggestion-review-process).' satisfies MarkdownString
|
||||
'Read more about the [listing statuses](/about#listing-statuses).' satisfies MarkdownString
|
||||
|
||||
export const {
|
||||
dataArray: verificationStatuses,
|
||||
|
||||
@@ -81,7 +81,8 @@ const announcement = await Astro.locals.banners.try(
|
||||
</head>
|
||||
<body
|
||||
class={cn('bg-night-700 text-day-300 flex min-h-dvh flex-col *:shrink-0', classNames?.body)}
|
||||
data-is-error-page={isErrorPage}
|
||||
data-is-error-page={isErrorPage ? '' : undefined}
|
||||
data-is-logged-in={Astro.locals.user !== null ? '' : undefined}
|
||||
>
|
||||
{announcement && <AnnouncementBanner announcement={announcement} transition:name="header-announcement" />}
|
||||
<Header
|
||||
|
||||
@@ -7,7 +7,7 @@ import { kycLevels } from '../constants/kycLevels'
|
||||
import { serviceVisibilitiesById } from '../constants/serviceVisibility'
|
||||
import { READ_MORE_SENTENCE_LINK, verificationStatusesByValue } from '../constants/verificationStatus'
|
||||
|
||||
import { formatDateShort } from './timeAgo'
|
||||
import { formatDaysAgo } from './timeAgo'
|
||||
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
@@ -199,7 +199,7 @@ export const nonDbAttributes: NonDbAttributeFull[] = [
|
||||
links: [],
|
||||
customize: (service) => ({
|
||||
show: service.isRecentlyApproved,
|
||||
description: `Approved on KYCnot.me ${formatDateShort(service.approvedAt ?? service.createdAt)}. Proceed with caution.`,
|
||||
description: `Approved on KYCnot.me less than 15 days ago${service.approvedAt ? ` (${formatDaysAgo(service.approvedAt)})` : ''}. Proceed with caution.`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,11 +7,15 @@ export function supportsBrowserNotifications() {
|
||||
}
|
||||
|
||||
export function isBrowserNotificationsEnabled() {
|
||||
return (
|
||||
supportsBrowserNotifications() &&
|
||||
Notification.permission === 'granted' &&
|
||||
typedLocalStorage.browserNotificationsEnabled.get()
|
||||
)
|
||||
const browserNotificationsEnabled = typedLocalStorage.browserNotificationsEnabled.get()
|
||||
if (!browserNotificationsEnabled) return false
|
||||
|
||||
if (!document.body.hasAttribute('data-is-logged-in')) {
|
||||
typedLocalStorage.browserNotificationsEnabled.set(false)
|
||||
return false
|
||||
}
|
||||
|
||||
return supportsBrowserNotifications() && Notification.permission === 'granted'
|
||||
}
|
||||
|
||||
export async function enableBrowserNotifications(): Promise<SafeResult> {
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { actions } from 'astro:actions'
|
||||
|
||||
type ServerSubscription = {
|
||||
endpoint: string
|
||||
userAgent: string | null
|
||||
}
|
||||
|
||||
export type SafeResult =
|
||||
@@ -45,7 +44,6 @@ export async function subscribeToPushNotifications(vapidPublicKey: string): Prom
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
endpoint: subscription.endpoint,
|
||||
userAgent: navigator.userAgent,
|
||||
p256dhKey: p256dh ? btoa(String.fromCharCode(...new Uint8Array(p256dh))) : '',
|
||||
authKey: auth ? btoa(String.fromCharCode(...new Uint8Array(auth))) : '',
|
||||
} satisfies ActionInput<typeof actions.notification.webPush.subscribe>),
|
||||
@@ -131,13 +129,7 @@ export async function isCurrentDeviceSubscribed(serverSubscriptions: ServerSubsc
|
||||
const currentSubscription = await getCurrentSubscription()
|
||||
if (!currentSubscription || serverSubscriptions.length === 0) return false
|
||||
|
||||
const currentEndpoint = currentSubscription.endpoint
|
||||
const currentUserAgent = navigator.userAgent
|
||||
|
||||
return serverSubscriptions.some(
|
||||
(sub) =>
|
||||
sub.endpoint === currentEndpoint && (sub.userAgent === currentUserAgent || sub.userAgent === null)
|
||||
)
|
||||
return serverSubscriptions.some((sub) => sub.endpoint === currentSubscription.endpoint)
|
||||
}
|
||||
|
||||
function urlB64ToUint8Array(base64String: string) {
|
||||
@@ -183,5 +175,5 @@ export function parsePushSubscriptions(subscriptionsAsString: string | undefined
|
||||
function isServerSubscription(subscription: unknown): subscription is ServerSubscription {
|
||||
if (typeof subscription !== 'object' || subscription === null) return false
|
||||
const s = subscription as Record<string, unknown>
|
||||
return typeof s.endpoint === 'string' && (typeof s.userAgent === 'string' || s.userAgent === null)
|
||||
return typeof s.endpoint === 'string'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { prisma } from './prisma'
|
||||
|
||||
import type { Prisma } from '@prisma/client'
|
||||
|
||||
export const MAX_COMMENT_DEPTH = 12
|
||||
@@ -75,12 +77,13 @@ export type CommentWithRepliesPopulated = CommentWithReplies<{
|
||||
export const commentSortSchema = z.enum(['newest', 'upvotes', 'status']).default('newest')
|
||||
export type CommentSortOption = z.infer<typeof commentSortSchema>
|
||||
|
||||
export function makeCommentsNestedQuery({
|
||||
export async function makeCommentsNestedQuery({
|
||||
depth = 0,
|
||||
user,
|
||||
showPending,
|
||||
serviceId,
|
||||
sort,
|
||||
highlightedCommentId,
|
||||
}: {
|
||||
depth?: number
|
||||
user: Prisma.UserGetPayload<{
|
||||
@@ -91,6 +94,7 @@ export function makeCommentsNestedQuery({
|
||||
showPending?: boolean
|
||||
serviceId: number
|
||||
sort: CommentSortOption
|
||||
highlightedCommentId?: number | null
|
||||
}) {
|
||||
const orderByClause: Prisma.CommentOrderByWithRelationInput[] = []
|
||||
|
||||
@@ -108,6 +112,8 @@ export function makeCommentsNestedQuery({
|
||||
}
|
||||
orderByClause.unshift({ suspicious: 'asc' }) // Always put suspicious comments last within a sort group
|
||||
|
||||
const highlightedBranchIds = highlightedCommentId ? await findAllParentIds(highlightedCommentId, depth) : []
|
||||
|
||||
const baseQuery = {
|
||||
...commentReplyQuery,
|
||||
orderBy: orderByClause,
|
||||
@@ -121,6 +127,9 @@ export function makeCommentsNestedQuery({
|
||||
: ({
|
||||
status: { in: ['APPROVED', 'VERIFIED'] },
|
||||
} as const satisfies Prisma.CommentWhereInput),
|
||||
...(highlightedBranchIds.length > 0
|
||||
? [{ id: { in: highlightedBranchIds } } as const satisfies Prisma.CommentWhereInput]
|
||||
: []),
|
||||
],
|
||||
parentId: null,
|
||||
serviceId,
|
||||
@@ -161,6 +170,47 @@ export function makeRepliesQuery<T extends Prisma.CommentFindManyArgs>(
|
||||
}
|
||||
}
|
||||
|
||||
async function findAllParentIds(commentId: number, depth: number) {
|
||||
const commentwithManyParents = await prisma.comment.findFirst({
|
||||
where: { id: commentId },
|
||||
select: makeParentQuerySelect(depth),
|
||||
})
|
||||
|
||||
return extractParentIds(commentwithManyParents, [commentId])
|
||||
}
|
||||
|
||||
type ParentQueryRecursive = {
|
||||
parent: {
|
||||
select: {
|
||||
id: true
|
||||
parent: false | { select: ParentQueryRecursive }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeParentQuerySelect(depth: number): ParentQueryRecursive {
|
||||
return {
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
parent: depth <= 0 ? false : { select: makeParentQuerySelect(depth - 1) },
|
||||
},
|
||||
},
|
||||
} as const satisfies Prisma.CommentSelect
|
||||
}
|
||||
|
||||
function extractParentIds(
|
||||
comment: Prisma.CommentGetPayload<{ select: ParentQueryRecursive }> | null,
|
||||
acc: number[] = []
|
||||
) {
|
||||
if (!comment?.parent?.id) return acc
|
||||
|
||||
return extractParentIds(comment.parent as Prisma.CommentGetPayload<{ select: ParentQueryRecursive }>, [
|
||||
...acc,
|
||||
comment.parent.id,
|
||||
])
|
||||
}
|
||||
|
||||
export function makeCommentUrl({
|
||||
serviceSlug,
|
||||
commentId,
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function stopImpersonating(context: Pick<APIContext, 'cookies' | 'l
|
||||
const sessionId = context.cookies.get(IMPERSONATION_SESSION_COOKIE)?.value
|
||||
await redisImpersonationSessions.delete(sessionId)
|
||||
context.cookies.delete(IMPERSONATION_SESSION_COOKIE)
|
||||
context.locals.user = context.locals.actualUser
|
||||
context.locals.user = context.locals.actualUser ?? context.locals.user
|
||||
context.locals.actualUser = null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addDays, format, isBefore, isToday, isYesterday } from 'date-fns'
|
||||
import { addDays, differenceInDays, format, isBefore, isToday, isYesterday } from 'date-fns'
|
||||
import TimeAgo from 'javascript-time-ago'
|
||||
import en from 'javascript-time-ago/locale/en'
|
||||
|
||||
@@ -47,3 +47,10 @@ export function formatDateShort(
|
||||
|
||||
return transformCase(text, caseType)
|
||||
}
|
||||
|
||||
export function formatDaysAgo(approvedAt: Date) {
|
||||
const days = differenceInDays(new Date(), approvedAt)
|
||||
if (days === 0) return 'today'
|
||||
if (days === 1) return 'yesterday'
|
||||
return `${days.toLocaleString()} days ago`
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function removeUserSessionIdCookie(cookies: AstroCookies) {
|
||||
cookies.delete(COOKIE_NAME, { path: '/' })
|
||||
}
|
||||
|
||||
export async function logout(context: Pick<APIContext, 'cookies' | 'locals'>) {
|
||||
export async function logout(context: Pick<APIContext, 'cookies' | 'locals' | 'request' | 'url'>) {
|
||||
await stopImpersonating(context)
|
||||
|
||||
await removeUserSessionIdCookie(context.cookies)
|
||||
|
||||
@@ -8,6 +8,21 @@ icon: 'ri:information-line'
|
||||
|
||||
import DonationAddress from '../components/DonationAddress.astro'
|
||||
|
||||
- [What is KYC?](#what-is-kyc)
|
||||
- [Why does this site exist?](#why-does-this-site-exist)
|
||||
- [Why only Bitcoin and Monero?](#why-only-bitcoin-and-monero)
|
||||
- [User Accounts](#user-accounts)
|
||||
- [Verified and Affiliated Users](#verified-and-affiliated-users)
|
||||
- [Listings](#listings)
|
||||
- [Suggesting a new listing](#suggesting-a-new-listing)
|
||||
- [Listing statuses](#listing-statuses)
|
||||
- [Reviews and Comments](#reviews-and-comments)
|
||||
- [Moderation](#moderation)
|
||||
- [API](#api)
|
||||
- [Donate](#support)
|
||||
- [Contact](#contact)
|
||||
- [Downloads and Assets](#downloads-and-assets)
|
||||
|
||||
## What is this page?
|
||||
|
||||
KYCnot.me is a directory of trustworthy alternatives for buying, exchanging, trading, and using cryptocurrencies without having to disclose your identity, thus preserving your right to privacy.
|
||||
@@ -52,9 +67,9 @@ Users earn karma by participating in the community. When your comments get appro
|
||||
|
||||
### Verified and Affiliated Users
|
||||
|
||||
Some users are **verified**, this means that the moderators have confirmed that the user is related to a specific link or service. The verification is directly linked to the URL, ensuring that the person behind the username has a legitimate connection to the service they claim to represent. This verification process is reserved for individuals with established reputation.
|
||||
**Verified users** have proven their identity by linking their account to a specific website. This verification confirms they are legitimate representatives of that site, whether it's a personal website, blog, social media profile, or service page. You can request verification [in your profile](/account).
|
||||
|
||||
Users can also be **affiliated** with a service if they're related to it, such as being an owner or part of the team. If you own a service and want to get verified, just reach out to us.
|
||||
**Affiliated users** are users who represent a service listed in the directory, such as owners, support staff, or team members. If you represent a service and want to become affiliated, you can request it [in your profile](/account).
|
||||
|
||||
## Listings
|
||||
|
||||
@@ -119,7 +134,7 @@ If the service meets all requirements, it is granted the **`VERIFIED`** status.
|
||||
|
||||
##### Failed Verifications (Scams)
|
||||
|
||||
If the data is not accurate, the service is a scam, or any other checks fail, the service will be rejected and will appear with a disclaimer.
|
||||
If the data is not accurate, the service is a scam, or any other checks fail, the service will be rejected and will appear with a disclaimer.
|
||||
|
||||
#### Verification Steps
|
||||
|
||||
@@ -184,6 +199,32 @@ There are two types of events:
|
||||
|
||||
You can also take a look at the [global timeline](/events) where you will find all the service's events sorted by date.
|
||||
|
||||
### Listing Statuses
|
||||
|
||||
#### **Unlisted**
|
||||
|
||||
Initial state after the service is submitted. The service will **not appear in the list or search results**. Only accessible with a **direct link**. An **initial review** is done by the team to ensure the service is **not spam or inappropriate**.
|
||||
|
||||
#### **Community Contributed**
|
||||
|
||||
The service is **listed** in the directory, but it has **not been reviewed** by our team. The information **may be inaccurate, incomplete, or fraudulent**. Users should use these services **with caution**.
|
||||
|
||||
#### **Approved**
|
||||
|
||||
The service is listed in the directory and has been **reviewed by our team**. The information is **accurate and complete**. Initial tests were **successful**, but there is **not enough trust** to be verified.
|
||||
|
||||
#### **Verified**
|
||||
|
||||
The service has been listed for a while and has **not been reported** as a scam, user reviews are **mostly positive**, and the service is **not under any investigation**. Further tests and checks have been **successfully completed**. Contact with support or admins was also successful.
|
||||
|
||||
#### **Scam**
|
||||
|
||||
The service is a **scam**. User reports, negative reviews, or **failed internal testing** and other red flags were found. Evidence is provided in the **verification section** of the service page.
|
||||
|
||||
#### **Archived**
|
||||
|
||||
The service is **no longer available**. It may have been **shut down, acquired, or otherwise discontinued**. Still **visible** in the directory **for reference**.
|
||||
|
||||
## Reviews and Comments
|
||||
|
||||
Reviews are comments with a one to five star rating for the service. Each user can leave only one review per service; new reviews replace the old one.
|
||||
@@ -194,21 +235,38 @@ If you've used the service, you can add an **order ID** or proof—this is only
|
||||
|
||||
Some reviews may be spam or fake. Read comments carefully and **always do your own research before making decisions**.
|
||||
|
||||
### Note on moderation
|
||||
### Moderation
|
||||
|
||||
**All comments are moderated.** First, an AI checks each comment. If nothing is flagged, the comment is published right away. If something seems off, the comment is held for a human to review. We only remove comments that are spam, nonsense, unsupported accusations, doxxing, or clear rule violations.
|
||||
**All comments are moderated.** First, an **AI checks** each comment. If nothing is flagged, the comment is published right away. If something seems off, the comment is held for a **human review**. We only remove comments that do not follow the guidelines.
|
||||
|
||||
Comments from [**users affiliated with a service**](/about#verified-and-affiliated-users) are automatically approved on their own service page.
|
||||
|
||||
To **see comments waiting for moderation**, toggle the switch in the comments section. These comments show up with a yellow background and a "pending" label.
|
||||
|
||||
#### Comment Guidelines
|
||||
|
||||
We welcome honest, constructive discussion. However, any comment or review that contains the following will be **rejected**:
|
||||
|
||||
- **Spam** – promotional content, fake reviews, or coordinated campaigns.
|
||||
- **Doxxing** – harassment, threats, or disclosure of private information.
|
||||
- **Illegal content** – anything that encourages illegal activity.
|
||||
- **AI-generated text** – AI-written content is not allowed.
|
||||
- **Unrelated content** – content that is not related to the service.
|
||||
- **Personal discussion** – discussions not related to the service.
|
||||
|
||||
A review score may be **disabled** if it:
|
||||
|
||||
- Is not based on your own first-hand experience.
|
||||
- Contains demonstrably false or unverified claims.
|
||||
- Is not relevant to the service being reviewed.
|
||||
|
||||
## API
|
||||
|
||||
You can access basic service data via our public API.
|
||||
|
||||
See the [API page](/docs/api) for more details.
|
||||
You can access basic service data through our [public API](/docs/api).
|
||||
|
||||
## Support
|
||||
|
||||
If you like this project, you can **support** it through these methods:
|
||||
You can **support** our work through these methods:
|
||||
|
||||
<DonationAddress
|
||||
cryptoName="Monero"
|
||||
@@ -218,11 +276,11 @@ If you like this project, you can **support** it through these methods:
|
||||
|
||||
## Contact
|
||||
|
||||
You can contact via direct chat:
|
||||
You can contact us through SimpleX Chat:
|
||||
|
||||
- [SimpleX Chat](https://simplex.chat/contact#/?v=2&smp=smp%3A%2F%2F0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU%3D%40smp8.simplex.im%2FcgKHYUYnpAIVoGb9lxb0qEMEpvYIvc1O%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAIW_JSq8wOsLKG4Xv4O54uT2D_l8MJBYKQIFj1FjZpnU%253D%26srv%3Dbeccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion)
|
||||
|
||||
## Downloads and assets
|
||||
## Downloads and Assets
|
||||
|
||||
For logos and brand assets, visit our [downloads page](/downloads).
|
||||
|
||||
|
||||
@@ -692,12 +692,12 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<Button
|
||||
as="button"
|
||||
color="gray"
|
||||
variant="faded"
|
||||
size="sm"
|
||||
label="Cancel"
|
||||
onclick={`document.getElementById('edit-form-${index}').classList.toggle('hidden')`}
|
||||
data-cancel-button
|
||||
data-cancel-form-id={`edit-form-${index}`}
|
||||
/>
|
||||
<Button
|
||||
as="button"
|
||||
@@ -721,3 +721,22 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
////////////////////////////////////////////////////////////
|
||||
// Optional script for cancel buttons in attribute forms. //
|
||||
// Hides the edit form when cancel button is clicked. //
|
||||
////////////////////////////////////////////////////////////
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
document.querySelectorAll<HTMLButtonElement>('[data-cancel-button]').forEach((button) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
const formId = button.getAttribute('data-cancel-form-id')
|
||||
if (!formId) throw new Error('Form ID not found')
|
||||
const form = document.getElementById(formId)
|
||||
if (!form) throw new Error('Form not found')
|
||||
form.classList.add('hidden')
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -163,7 +163,6 @@ const [dbNotifications, notificationPreferences, totalNotifications, pushSubscri
|
||||
where: { userId: user.id },
|
||||
select: {
|
||||
endpoint: true,
|
||||
userAgent: true,
|
||||
},
|
||||
}),
|
||||
[],
|
||||
|
||||
Reference in New Issue
Block a user