import webpush from 'web-push'; import { db } from './db'; import { notificationSubscription } from './db/schema'; import type { NotificationSubscriptionInsert } from './db/schema'; import { eq, and } from 'drizzle-orm'; import { nanoid } from 'nanoid'; import { VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT } from '$env/static/private'; // Initialize web-push with VAPID keys if (!VAPID_PUBLIC_KEY || !VAPID_PRIVATE_KEY || !VAPID_SUBJECT) { throw new Error( 'VAPID keys are not configured. Please set VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT in your environment variables.' ); } webpush.setVapidDetails(VAPID_SUBJECT, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY); export interface PushSubscriptionData { endpoint: string; keys: { p256dh: string; auth: string; }; } export interface PushNotificationPayload { title: string; message: string; data?: Record; icon?: string; badge?: string; tag?: string; url?: string; } export class PushService { /** * Save a push subscription for a user */ async saveSubscription( userId: string, subscription: PushSubscriptionData, userAgent?: string ): Promise { const subscriptionData: NotificationSubscriptionInsert = { id: nanoid(), userId, endpoint: subscription.endpoint, p256dhKey: subscription.keys.p256dh, authKey: subscription.keys.auth, userAgent: userAgent || null, isActive: true }; // Check if subscription already exists const existing = await db .select() .from(notificationSubscription) .where(eq(notificationSubscription.endpoint, subscription.endpoint)) .limit(1); if (existing.length > 0) { // Update existing subscription await db .update(notificationSubscription) .set({ p256dhKey: subscription.keys.p256dh, authKey: subscription.keys.auth, userAgent: userAgent || null, isActive: true, updatedAt: new Date() }) .where(eq(notificationSubscription.endpoint, subscription.endpoint)); } else { // Insert new subscription await db.insert(notificationSubscription).values(subscriptionData); } } /** * Remove a push subscription */ async removeSubscription(userId: string, endpoint: string): Promise { await db .delete(notificationSubscription) .where( and( eq(notificationSubscription.userId, userId), eq(notificationSubscription.endpoint, endpoint) ) ); } /** * Get all active subscriptions for a user */ async getUserSubscriptions(userId: string) { return await db .select() .from(notificationSubscription) .where( and( eq(notificationSubscription.userId, userId), eq(notificationSubscription.isActive, true) ) ); } /** * Send push notification to a user's subscriptions */ async sendPushNotification(userId: string, payload: PushNotificationPayload): Promise { const subscriptions = await this.getUserSubscriptions(userId); if (subscriptions.length === 0) { console.log(`No active subscriptions found for user ${userId}`); return; } const notificationPayload = JSON.stringify({ title: payload.title, body: payload.message, icon: payload.icon || '/logo.svg', badge: payload.badge || '/logo.svg', tag: payload.tag, data: { url: payload.url || '/', ...payload.data } }); const sendPromises = subscriptions.map(async (sub) => { const pushSubscription: webpush.PushSubscription = { endpoint: sub.endpoint, keys: { p256dh: sub.p256dhKey, auth: sub.authKey } }; try { await webpush.sendNotification(pushSubscription, notificationPayload); } catch (error) { console.error(`Failed to send push notification to ${sub.endpoint}:`, error); // If the subscription is invalid (410 Gone or 404), deactivate it if (error instanceof Error && 'statusCode' in error) { const statusCode = (error as { statusCode: number }).statusCode; if (statusCode === 410 || statusCode === 404) { await db .update(notificationSubscription) .set({ isActive: false }) .where(eq(notificationSubscription.id, sub.id)); } } } }); await Promise.all(sendPromises); } /** * Send push notification to multiple users */ async sendPushNotificationToUsers( userIds: string[], payload: PushNotificationPayload ): Promise { const sendPromises = userIds.map((userId) => this.sendPushNotification(userId, payload)); await Promise.all(sendPromises); } /** * Deactivate a subscription */ async deactivateSubscription(endpoint: string): Promise { await db .update(notificationSubscription) .set({ isActive: false }) .where(eq(notificationSubscription.endpoint, endpoint)); } } export const pushService = new PushService(); export { VAPID_PUBLIC_KEY };