140 lines
4.8 KiB
TypeScript
140 lines
4.8 KiB
TypeScript
import webpush from 'web-push';
|
|
import logger from '@utils/logger';
|
|
import { Subscription } from '@models/Subscription';
|
|
import { Notification } from '@models/Notification';
|
|
|
|
type PushSubscription = any; // Web Push protocol JSON
|
|
|
|
interface NotificationPayload {
|
|
title: string;
|
|
body: string;
|
|
requestId?: string;
|
|
requestNumber?: string;
|
|
url?: string;
|
|
type?: string;
|
|
priority?: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
|
actionRequired?: boolean;
|
|
metadata?: any;
|
|
}
|
|
|
|
class NotificationService {
|
|
private userIdToSubscriptions: Map<string, PushSubscription[]> = new Map();
|
|
|
|
configure(vapidPublicKey?: string, vapidPrivateKey?: string, mailto?: string) {
|
|
const pub = vapidPublicKey || process.env.VAPID_PUBLIC_KEY || '';
|
|
const priv = vapidPrivateKey || process.env.VAPID_PRIVATE_KEY || '';
|
|
const contact = mailto || process.env.VAPID_CONTACT || 'mailto:admin@example.com';
|
|
if (!pub || !priv) {
|
|
logger.warn('VAPID keys are not configured. Push notifications are disabled.');
|
|
return;
|
|
}
|
|
webpush.setVapidDetails(contact, pub, priv);
|
|
logger.info('Web Push configured');
|
|
}
|
|
|
|
async addSubscription(userId: string, subscription: PushSubscription, userAgent?: string) {
|
|
// Persist to DB (upsert by endpoint)
|
|
try {
|
|
const endpoint: string = subscription?.endpoint || '';
|
|
const keys = subscription?.keys || {};
|
|
if (!endpoint || !keys?.p256dh || !keys?.auth) throw new Error('Invalid subscription payload');
|
|
await Subscription.upsert({
|
|
userId,
|
|
endpoint,
|
|
p256dh: keys.p256dh,
|
|
auth: keys.auth,
|
|
userAgent: userAgent || null,
|
|
} as any);
|
|
} catch (e) {
|
|
logger.error('Failed to persist subscription', e);
|
|
}
|
|
const list = this.userIdToSubscriptions.get(userId) || [];
|
|
const already = list.find((s) => JSON.stringify(s) === JSON.stringify(subscription));
|
|
if (!already) {
|
|
list.push(subscription);
|
|
this.userIdToSubscriptions.set(userId, list);
|
|
}
|
|
logger.info(`Subscription stored for user ${userId}. Total: ${list.length}`);
|
|
}
|
|
|
|
/**
|
|
* Send notification to users - saves to DB and sends via push/socket
|
|
*/
|
|
async sendToUsers(userIds: string[], payload: NotificationPayload) {
|
|
const message = JSON.stringify(payload);
|
|
const sentVia: string[] = ['IN_APP']; // Always save to DB for in-app display
|
|
|
|
for (const userId of userIds) {
|
|
try {
|
|
// 1. Save notification to database for in-app display
|
|
const notification = await Notification.create({
|
|
userId,
|
|
requestId: payload.requestId,
|
|
notificationType: payload.type || 'general',
|
|
title: payload.title,
|
|
message: payload.body,
|
|
isRead: false,
|
|
priority: payload.priority || 'MEDIUM',
|
|
actionUrl: payload.url,
|
|
actionRequired: payload.actionRequired || false,
|
|
metadata: {
|
|
requestNumber: payload.requestNumber,
|
|
...payload.metadata
|
|
},
|
|
sentVia,
|
|
emailSent: false,
|
|
smsSent: false,
|
|
pushSent: false
|
|
} as any);
|
|
|
|
logger.info(`[Notification] Created in-app notification for user ${userId}: ${payload.title}`);
|
|
|
|
// 2. Emit real-time socket event for immediate delivery
|
|
try {
|
|
const { emitToUser } = require('../realtime/socket');
|
|
if (emitToUser) {
|
|
emitToUser(userId, 'notification:new', {
|
|
notification: notification.toJSON(),
|
|
...payload
|
|
});
|
|
logger.info(`[Notification] Emitted socket event to user ${userId}`);
|
|
}
|
|
} catch (socketError) {
|
|
logger.warn(`[Notification] Socket emit failed (not critical):`, socketError);
|
|
}
|
|
|
|
// 3. Send push notification (if user has subscriptions)
|
|
let subs = this.userIdToSubscriptions.get(userId) || [];
|
|
// Load from DB if memory empty
|
|
if (subs.length === 0) {
|
|
try {
|
|
const rows = await Subscription.findAll({ where: { userId } });
|
|
subs = rows.map((r: any) => ({ endpoint: r.endpoint, keys: { p256dh: r.p256dh, auth: r.auth } }));
|
|
} catch {}
|
|
}
|
|
|
|
if (subs.length > 0) {
|
|
for (const sub of subs) {
|
|
try {
|
|
await webpush.sendNotification(sub, message);
|
|
await notification.update({ pushSent: true });
|
|
logger.info(`[Notification] Push sent to user ${userId}`);
|
|
} catch (err) {
|
|
logger.error(`Failed to send push to ${userId}:`, err);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error(`[Notification] Failed to create notification for user ${userId}:`, error);
|
|
// Continue to next user even if one fails
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const notificationService = new NotificationService();
|
|
notificationService.configure();
|
|
|
|
|
|
|