From f3c9b92ddbecd48f1e792c263b2a68eb342d9546 Mon Sep 17 00:00:00 2001 From: pluja Date: Sun, 15 Jun 2025 13:18:22 +0000 Subject: [PATCH] Release 202506151318 --- web/package-lock.json | 48 ++++++++--------- .../migration.sql | 8 +++ web/prisma/schema.prisma | 2 - web/src/actions/notifications.ts | 15 +++--- web/src/components/BaseHead.astro | 8 +-- web/src/components/CommentSection.astro | 3 +- .../components/PushNotificationBanner.astro | 1 - web/src/components/ServerEventsScript.astro | 6 +++ web/src/components/ServiceWorkerScript.astro | 10 +++- .../VerificationWarningBanner.astro | 13 +---- web/src/layouts/BaseLayout.astro | 3 +- web/src/lib/attributes.ts | 4 +- web/src/lib/client/browserNotifications.ts | 14 +++-- web/src/lib/client/clientPushNotifications.ts | 12 +---- web/src/lib/commentsWithReplies.ts | 52 ++++++++++++++++++- web/src/lib/impersonation.ts | 2 +- web/src/lib/timeAgo.ts | 9 +++- web/src/lib/userCookies.ts | 2 +- web/src/pages/admin/attributes.astro | 23 +++++++- web/src/pages/notifications.astro | 1 - 20 files changed, 159 insertions(+), 77 deletions(-) create mode 100644 web/prisma/migrations/20250615123257_remove_user_agent/migration.sql diff --git a/web/package-lock.json b/web/package-lock.json index 489ec27..8ddd59a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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": { diff --git a/web/prisma/migrations/20250615123257_remove_user_agent/migration.sql b/web/prisma/migrations/20250615123257_remove_user_agent/migration.sql new file mode 100644 index 0000000..953aed1 --- /dev/null +++ b/web/prisma/migrations/20250615123257_remove_user_agent/migration.sql @@ -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"; diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index 403b2c6..3fc31d1 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -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 diff --git a/web/src/actions/notifications.ts b/web/src/actions/notifications.ts index 3a039bf..d2c5664 100644 --- a/web/src/actions/notifications.ts +++ b/web/src/actions/notifications.ts @@ -1,3 +1,4 @@ +import { ActionError } from 'astro:actions' import { z } from 'astro:content' import { defineProtectedAction } from '../lib/defineProtectedAction' @@ -32,7 +33,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 +43,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,7 +56,7 @@ export const notificationActions = { unsubscribe: defineProtectedAction({ accept: 'json', - permissions: 'user', + permissions: 'guest', input: z.object({ endpoint: z.string().optional(), }), @@ -66,16 +64,21 @@ export const notificationActions = { if (input.endpoint) { await prisma.pushSubscription.deleteMany({ where: { - userId: context.locals.user.id, + userId: context.locals.user?.id ?? undefined, endpoint: input.endpoint, }, }) - } else { + } else if (context.locals.user) { await prisma.pushSubscription.deleteMany({ where: { userId: context.locals.user.id, }, }) + } else { + throw new ActionError({ + code: 'BAD_REQUEST', + message: 'Endpoint is required when user is not logged in.', + }) } }, }), diff --git a/web/src/components/BaseHead.astro b/web/src/components/BaseHead.astro index 0daf884..bdba77c 100644 --- a/web/src/components/BaseHead.astro +++ b/web/src/components/BaseHead.astro @@ -107,12 +107,8 @@ const ogImageUrl = makeOgImageUrl(ogImage, Astro.url) - -{ - !Astro.url.pathname.startsWith('/admin') && ( - - ) /* Disable to prevent bugs in important admin forms */ -} + + {htmx && } diff --git a/web/src/components/CommentSection.astro b/web/src/components/CommentSection.astro index 49422f0..bc8d3db 100644 --- a/web/src/components/CommentSection.astro +++ b/web/src/components/CommentSection.astro @@ -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, }) ), [], diff --git a/web/src/components/PushNotificationBanner.astro b/web/src/components/PushNotificationBanner.astro index c880ae4..df392f9 100644 --- a/web/src/components/PushNotificationBanner.astro +++ b/web/src/components/PushNotificationBanner.astro @@ -16,7 +16,6 @@ type Props = HTMLAttributes<'div'> & { pushSubscriptions: Prisma.PushSubscriptionGetPayload<{ select: { endpoint: true - userAgent: true } }>[] } diff --git a/web/src/components/ServerEventsScript.astro b/web/src/components/ServerEventsScript.astro index 77cd488..bc7ec82 100644 --- a/web/src/components/ServerEventsScript.astro +++ b/web/src/components/ServerEventsScript.astro @@ -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) diff --git a/web/src/components/ServiceWorkerScript.astro b/web/src/components/ServiceWorkerScript.astro index b696096..5d407ca 100644 --- a/web/src/components/ServiceWorkerScript.astro +++ b/web/src/components/ServiceWorkerScript.astro @@ -4,6 +4,7 @@ diff --git a/web/src/components/VerificationWarningBanner.astro b/web/src/components/VerificationWarningBanner.astro index 43cb522..eb01826 100644 --- a/web/src/components/VerificationWarningBanner.astro +++ b/web/src/components/VerificationWarningBanner.astro @@ -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` -} --- { @@ -73,7 +64,7 @@ function formatApprovedAt(approvedAt: Date | null) { ) : service.isRecentlyApproved ? (
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. {announcement && }
({ 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.`, }), }, { diff --git a/web/src/lib/client/browserNotifications.ts b/web/src/lib/client/browserNotifications.ts index 9dab89a..8d0adf8 100644 --- a/web/src/lib/client/browserNotifications.ts +++ b/web/src/lib/client/browserNotifications.ts @@ -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 { diff --git a/web/src/lib/client/clientPushNotifications.ts b/web/src/lib/client/clientPushNotifications.ts index 917b18c..f3eec25 100644 --- a/web/src/lib/client/clientPushNotifications.ts +++ b/web/src/lib/client/clientPushNotifications.ts @@ -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), @@ -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 - return typeof s.endpoint === 'string' && (typeof s.userAgent === 'string' || s.userAgent === null) + return typeof s.endpoint === 'string' } diff --git a/web/src/lib/commentsWithReplies.ts b/web/src/lib/commentsWithReplies.ts index 5d6ad89..d354db1 100644 --- a/web/src/lib/commentsWithReplies.ts +++ b/web/src/lib/commentsWithReplies.ts @@ -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 -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( } } +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, diff --git a/web/src/lib/impersonation.ts b/web/src/lib/impersonation.ts index 2bd4e2e..b238f1e 100644 --- a/web/src/lib/impersonation.ts +++ b/web/src/lib/impersonation.ts @@ -29,7 +29,7 @@ export async function stopImpersonating(context: Pick) { +export async function logout(context: Pick) { await stopImpersonating(context) await removeUserSessionIdCookie(context.cookies) diff --git a/web/src/pages/admin/attributes.astro b/web/src/pages/admin/attributes.astro index 0fd432c..4ee2569 100644 --- a/web/src/pages/admin/attributes.astro +++ b/web/src/pages/admin/attributes.astro @@ -692,12 +692,12 @@ const makeSortUrl = (slug: NonNullable<(typeof filters)['sort-by']>) => {
+ + diff --git a/web/src/pages/notifications.astro b/web/src/pages/notifications.astro index fae0fbe..3131829 100644 --- a/web/src/pages/notifications.astro +++ b/web/src/pages/notifications.astro @@ -163,7 +163,6 @@ const [dbNotifications, notificationPreferences, totalNotifications, pushSubscri where: { userId: user.id }, select: { endpoint: true, - userAgent: true, }, }), [],