CognxSafeTrack
feat(billing): complete billing system, push notifications, and tech debt fixes
8280d7d | 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(() => {}); | |
| } | |
| } | |
| } | |
| } | |
| }; | |