Files
buzz-sheet/lib/commission/push.ts
T
gunshiz 3473b55768
CI / Verify (push) Successful in 1m48s
CI / Build immutable images and deploy (push) Successful in 2m47s
feat(commission) : add push notifications and unread ticket titles
2026-10-02 11:51:05 +07:00

59 lines
2.7 KiB
TypeScript

import "server-only";
import { and, eq, ne, or } from "drizzle-orm";
import webPush from "web-push";
import { getDb } from "@/db";
import { commissionPushSubscriptions, users } from "@/db/schema";
export function pushPublicKey() {
return process.env.WEB_PUSH_PUBLIC_KEY && process.env.WEB_PUSH_PRIVATE_KEY && process.env.WEB_PUSH_SUBJECT
? process.env.WEB_PUSH_PUBLIC_KEY : null;
}
export function validPushEndpoint(value: string) {
try {
const url = new URL(value);
const host = url.hostname.toLowerCase();
return url.protocol === "https:" && !url.username && !url.password &&
(host === "fcm.googleapis.com" || host === "updates.push.services.mozilla.com" ||
host === "web.push.apple.com" || host.endsWith(".push.apple.com") ||
host.endsWith(".notify.windows.com"));
} catch { return false; }
}
export async function sendCommissionMessagePush(message: {
id: string; ticketId: string; ticketTitle: string; customerId: string; authorId: string;
authorName: string; text: string | null;
}) {
if (!pushPublicKey()) return;
const subscriptions = await getDb().select({ subscription: commissionPushSubscriptions })
.from(commissionPushSubscriptions)
.innerJoin(users, eq(users.id, commissionPushSubscriptions.userId))
.where(and(ne(commissionPushSubscriptions.userId, message.authorId), eq(users.banned, false),
or(and(eq(commissionPushSubscriptions.scope, "customer"),
eq(commissionPushSubscriptions.userId, message.customerId)),
and(eq(commissionPushSubscriptions.scope, "admin"), eq(users.role, "admin"),
eq(users.emailVerified, true)))));
webPush.setVapidDetails(process.env.WEB_PUSH_SUBJECT!, process.env.WEB_PUSH_PUBLIC_KEY!,
process.env.WEB_PUSH_PRIVATE_KEY!);
for (let offset = 0; offset < subscriptions.length; offset += 10) {
await Promise.all(subscriptions.slice(offset, offset + 10).map(async ({ subscription }) => {
const payload = JSON.stringify({
id: message.id, title: `${message.authorName} · ${message.ticketTitle}`,
body: message.text?.slice(0, 300) || "ส่งรูปภาพ",
url: `/${subscription.scope === "admin" ? "admin/commission" : "commission/tickets"}/${message.ticketId}`,
});
try {
await webPush.sendNotification({ endpoint: subscription.endpoint,
keys: { p256dh: subscription.p256dh, auth: subscription.auth } }, payload,
{ TTL: 60 * 60 * 24, timeout: 5000 });
} catch (cause) {
if (cause && typeof cause === "object" && "statusCode" in cause &&
(cause.statusCode === 404 || cause.statusCode === 410))
await getDb().delete(commissionPushSubscriptions)
.where(eq(commissionPushSubscriptions.endpoint, subscription.endpoint));
}
}));
}
}