File size: 4,144 Bytes
b43e552 7b0c22b b43e552 8280d7d b43e552 7b0c22b b43e552 7b0c22b b43e552 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | import webpush from 'web-push';
import { prisma } from './prisma';
import { logger } from '../logger';
// VAPID keys should be in .env. If not, we log a warning.
const publicVapidKey = process.env.VAPID_PUBLIC_KEY;
const privateVapidKey = process.env.VAPID_PRIVATE_KEY;
const contactEmail = process.env.VAPID_EMAIL || 'mailto:support@xamle.studio';
if (publicVapidKey && privateVapidKey) {
webpush.setVapidDetails(contactEmail, publicVapidKey, privateVapidKey);
} else {
logger.warn('[PUSH-SERVICE] VAPID keys are missing. Push notifications will not work.');
// To generate keys, you can use: webpush.generateVAPIDKeys()
}
export const pushService = {
/**
* Store a new subscription for a user
*/
async subscribe(userId: string, organizationId: string, subscription: any) {
return prisma.pushSubscription.upsert({
where: { endpoint: subscription.endpoint },
update: {
userId,
organizationId,
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth
},
create: {
userId,
organizationId,
endpoint: subscription.endpoint,
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth
}
});
},
/**
* Send a notification to all active subscriptions of a specific user
*/
async sendToUser(userId: string, organizationId: string, title: string, body: string, icon?: string, data?: Record<string, unknown>) {
const subscriptions = await prisma.pushSubscription.findMany({ where: { userId, organizationId } });
const payload = JSON.stringify({
title,
body,
icon: icon || 'https://xamle.studio/logo.png',
data,
timestamp: Date.now(),
});
const results = await Promise.allSettled(
subscriptions.map((sub: any) => {
const pushConfig = { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } };
return webpush.sendNotification(pushConfig, payload);
})
);
for (let i = 0; i < results.length; i++) {
if (results[i].status === 'rejected') {
const error = (results[i] as PromiseRejectedResult).reason;
if (error.statusCode === 410 || error.statusCode === 404) {
await prisma.pushSubscription.delete({ where: { id: subscriptions[i].id } }).catch(() => {});
}
}
}
},
/**
* Send a notification to all active subscriptions of an organization
*/
async notifyOrganization(organizationId: string, title: string, body: string, icon?: string) {
const subscriptions = await prisma.pushSubscription.findMany({
where: { organizationId }
});
const payload = JSON.stringify({
title,
body,
icon: icon || 'https://xamle.studio/logo.png',
timestamp: Date.now()
});
const results = await Promise.allSettled(
subscriptions.map((sub: any) => {
const pushConfig = {
endpoint: sub.endpoint,
keys: {
p256dh: sub.p256dh,
auth: sub.auth
}
};
return webpush.sendNotification(pushConfig, payload);
})
);
// Clean up failed subscriptions (e.g. expired or unsubscribed)
for (let i = 0; i < results.length; i++) {
if (results[i].status === 'rejected') {
const error = (results[i] as PromiseRejectedResult).reason;
if (error.statusCode === 410 || error.statusCode === 404) {
logger.info(`[PUSH-SERVICE] Removing expired subscription: ${subscriptions[i].endpoint}`);
await prisma.pushSubscription.delete({ where: { id: subscriptions[i].id } }).catch(() => {});
}
}
}
}
};
|