From c88280d9f4bc391b43c8be4524f18e25e9ee58e2 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:19:32 +0330 Subject: [PATCH] Import source: app/backend/src/lib/push Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/backend/src/lib/push.ts | 108 ++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 app/backend/src/lib/push.ts diff --git a/app/backend/src/lib/push.ts b/app/backend/src/lib/push.ts new file mode 100644 index 0000000..909a701 --- /dev/null +++ b/app/backend/src/lib/push.ts @@ -0,0 +1,108 @@ +import webpush from 'web-push'; +import type { NotificationType } from '@prisma/client'; +import { prisma } from '../db.js'; + +if (process.env.VAPID_PUBLIC_KEY && process.env.VAPID_PRIVATE_KEY) { + webpush.setVapidDetails( + process.env.VAPID_SUBJECT ?? 'mailto:admin@teknav.ir', + process.env.VAPID_PUBLIC_KEY, + process.env.VAPID_PRIVATE_KEY, + ); +} + +function buildPushPayload(type: NotificationType | string, payload: Record) { + const icon = '/icons/icon-192x192.png'; + const map: Record = { + new_article: { + title: `مقاله جدید: ${payload.articleTitle ?? payload.title ?? ''}`, + body: String(payload.excerpt ?? ''), + url: `/article/${payload.articleSlug ?? payload.slug ?? ''}`, + }, + comment: { + title: 'نظر جدید روی مقاله شما', + body: String(payload.content ?? '').slice(0, 80), + url: `/article/${payload.articleSlug ?? ''}`, + }, + comment_reply: { + title: 'پاسخ به نظر شما', + body: String(payload.content ?? '').slice(0, 80), + url: `/article/${payload.articleSlug ?? ''}#comment-${payload.commentId ?? ''}`, + }, + review_approved: { + title: 'مقاله شما تأیید شد', + body: String(payload.articleTitle ?? payload.title ?? ''), + url: '/writer', + }, + review_revision: { + title: 'بازخورد ویراستار', + body: String(payload.note ?? ''), + url: '/writer', + }, + review_rejected: { + title: 'مقاله رد شد', + body: String(payload.articleTitle ?? payload.title ?? ''), + url: '/writer', + }, + review_submitted: { + title: 'مقاله جدید برای بررسی', + body: String(payload.articleTitle ?? payload.title ?? ''), + url: '/admin', + }, + system: { + title: 'تکناو', + body: String(payload.message ?? ''), + url: '/', + }, + }; + const base = map[type] ?? { title: 'تکناو', body: '', url: '/' }; + return { ...base, icon }; +} + +async function deliverPush( + subs: { endpoint: string; auth: string; p256dh: string }[], + data: string, +) { + await Promise.allSettled( + subs.map((sub) => + webpush + .sendNotification({ endpoint: sub.endpoint, keys: { auth: sub.auth, p256dh: sub.p256dh } }, data) + .catch(async (err: { statusCode?: number }) => { + if (err.statusCode === 410) { + await prisma.pushSubscription.delete({ where: { endpoint: sub.endpoint } }).catch(() => {}); + } + }), + ), + ); +} + +export async function sendPushToUser( + userId: string, + type: NotificationType, + payload: Record, +) { + if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) return; + + // Check notification preference — if user has explicitly disabled push for this event, skip. + const pref = await prisma.notificationPreference.findUnique({ + where: { userId_eventType_channel: { userId, eventType: type, channel: 'push' } }, + }).catch(() => null); + if (pref && !pref.enabled) return; + + const subs = await prisma.pushSubscription.findMany({ where: { userId } }); + if (!subs.length) return; + const data = JSON.stringify(buildPushPayload(type, payload)); + await deliverPush(subs, data); +} + +export async function sendGuestPushByTopic( + topic: string, + payload: Record, +) { + if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) return; + const subs = await prisma.pushSubscription.findMany({ + where: { userId: null, topics: { has: topic } }, + }); + if (!subs.length) return; + const data = JSON.stringify(buildPushPayload('new_article', payload)); + await deliverPush(subs, data); +}