Files
teknav-bak/app/backend/src/lib/push.ts
T
arsam c88280d9f4 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.
2026-09-18 23:19:32 +03:30

109 lines
3.6 KiB
TypeScript

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<string, unknown>) {
const icon = '/icons/icon-192x192.png';
const map: Record<string, { title: string; body: string; url: string }> = {
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<string, unknown>,
) {
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<string, unknown>,
) {
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);
}