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,
},
}),
[],