Re_Backend/src/services/notification.service.ts

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();