Let users see and unsubscribe their own push devices
Fixes duplicate/triple OS notifications on machines where more than one browser (e.g. both Chrome and Edge on Windows) independently got notification permission for the site — each held its own valid subscription, so every event pushed to both. Adds a userAgent column (captured client-side at subscribe time, display-only) and a Settings > Account section listing a user's own subscribed browsers with a friendly label plus an unsubscribe button, backed by new GET/DELETE /api/push/subscriptions endpoints scoped to the caller's own rows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
This commit is contained in:
1 parent
4d393aec91
commit
f5e49b5c53
11 files changed
+1374
-6
No files matched your search
+24
-4
@@ -1,4 +1,4 @@
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { eq, inArray, and } from "drizzle-orm";
|
||||
import { db } from "@/lib/db/client";
|
||||
import { pushSubscriptions, users } from "@/lib/db/schema";
|
||||
import type { TicketDTO, MessageDTO } from "@/lib/tickets/types";
|
||||
@@ -10,13 +10,17 @@ export interface PushSubscriptionKeys {
|
||||
}
|
||||
|
||||
/** Re-subscribing from the same browser (e.g. after clearing site data and re-granting) overwrites the old row via the endpoint's unique constraint, rather than accumulating stale duplicates. */
|
||||
export async function savePushSubscription(userId: string, sub: PushSubscriptionKeys): Promise<void> {
|
||||
export async function savePushSubscription(
|
||||
userId: string,
|
||||
sub: PushSubscriptionKeys,
|
||||
userAgent?: string,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.insert(pushSubscriptions)
|
||||
.values({ userId, endpoint: sub.endpoint, p256dh: sub.keys.p256dh, auth: sub.keys.auth })
|
||||
.values({ userId, endpoint: sub.endpoint, p256dh: sub.keys.p256dh, auth: sub.keys.auth, userAgent })
|
||||
.onConflictDoUpdate({
|
||||
target: pushSubscriptions.endpoint,
|
||||
set: { userId, p256dh: sub.keys.p256dh, auth: sub.keys.auth },
|
||||
set: { userId, p256dh: sub.keys.p256dh, auth: sub.keys.auth, userAgent },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,6 +28,22 @@ export async function deletePushSubscriptionByEndpoint(endpoint: string): Promis
|
||||
await db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint));
|
||||
}
|
||||
|
||||
/** A user's own subscribed devices/browsers, for the "manage notifications" settings list — never exposes the endpoint/keys, only what's useful to tell rows apart. */
|
||||
export async function listPushSubscriptionsForUser(userId: string) {
|
||||
const rows = await db.query.pushSubscriptions.findMany({
|
||||
where: eq(pushSubscriptions.userId, userId),
|
||||
orderBy: (t, { desc }) => desc(t.createdAt),
|
||||
});
|
||||
return rows.map((r) => ({ id: r.id, userAgent: r.userAgent, createdAt: r.createdAt }));
|
||||
}
|
||||
|
||||
/** Deletes one of the current user's own subscriptions by id — scoped so a user can only ever remove their own. */
|
||||
export async function deleteOwnPushSubscription(userId: string, id: string): Promise<void> {
|
||||
await db
|
||||
.delete(pushSubscriptions)
|
||||
.where(and(eq(pushSubscriptions.id, id), eq(pushSubscriptions.userId, userId)));
|
||||
}
|
||||
|
||||
async function sendPushToUsers(userIds: string[], payload: { title: string; body: string; url: string }): Promise<void> {
|
||||
if (userIds.length === 0 || !ensureVapidConfigured()) return;
|
||||
|
||||
|
||||
Reference in new issue
Block a user