'use server'; import webpush from 'web-push'; import { prisma } from '@/lib/db'; // Configure web-push with VAPID keys if (process.env.VAPID_PUBLIC_KEY && process.env.VAPID_PRIVATE_KEY && process.env.VAPID_SUBJECT) { webpush.setVapidDetails( process.env.VAPID_SUBJECT, process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY ); } export interface PushNotificationPayload { title: string; body: string; icon?: string; badge?: string; tag?: string; data?: { url?: string; listId?: string; itemId?: string; action?: string; }; } /** * Subscribe a user to push notifications */ export async function subscribeToPush(userId: string, subscription: PushSubscriptionJSON) { if (!subscription.endpoint || !subscription.keys?.p256dh || !subscription.keys?.auth) { throw new Error('Invalid subscription object'); } try { await prisma.pushSubscription.upsert({ where: { userId_endpoint: { userId, endpoint: subscription.endpoint, }, }, create: { userId, endpoint: subscription.endpoint, p256dh: subscription.keys.p256dh, auth: subscription.keys.auth, }, update: { p256dh: subscription.keys.p256dh, auth: subscription.keys.auth, updatedAt: new Date(), }, }); return { success: true }; } catch (error) { console.error('Failed to save push subscription:', error); throw new Error('Failed to subscribe to push notifications'); } } /** * Unsubscribe a user from push notifications */ export async function unsubscribeFromPush(userId: string, endpoint: string) { try { await prisma.pushSubscription.deleteMany({ where: { userId, endpoint, }, }); return { success: true }; } catch (error) { console.error('Failed to remove push subscription:', error); throw new Error('Failed to unsubscribe from push notifications'); } } /** * Send a push notification to a specific user */ export async function sendPushNotification(userId: string, payload: PushNotificationPayload) { if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) { console.warn('VAPID keys not configured, skipping push notification'); return { success: false, error: 'Push notifications not configured' }; } try { // Get all push subscriptions for the user const subscriptions = await prisma.pushSubscription.findMany({ where: { userId }, }); if (subscriptions.length === 0) { return { success: true, sent: 0 }; } // Send notification to all user's devices const results = await Promise.allSettled( subscriptions.map(async (sub) => { const pushSubscription = { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth, }, }; try { await webpush.sendNotification( pushSubscription, JSON.stringify(payload), { TTL: 60 * 60 * 24, // 24 hours } ); return { success: true }; } catch (error: any) { // Handle expired or invalid subscriptions if (error.statusCode === 410 || error.statusCode === 404) { // Subscription expired, remove it await prisma.pushSubscription.delete({ where: { id: sub.id }, }); } throw error; } }) ); const successful = results.filter((r) => r.status === 'fulfilled').length; return { success: true, sent: successful, total: subscriptions.length }; } catch (error) { console.error('Failed to send push notification:', error); return { success: false, error: 'Failed to send notification' }; } } /** * Send notification to all collaborators of a list */ export async function notifyListCollaborators( listId: string, payload: PushNotificationPayload, excludeUserId?: string ) { try { // Get list owner and collaborators const list = await prisma.list.findUnique({ where: { id: listId }, include: { collaborators: { select: { userId: true }, }, }, }); if (!list) { return { success: false, error: 'List not found' }; } // Collect all user IDs (owner + collaborators) const userIds = [ list.ownerId, ...list.collaborators.map((c) => c.userId), ].filter((id) => id !== excludeUserId); // Exclude the user who triggered the action // Send notifications to all users const results = await Promise.allSettled( userIds.map((userId) => sendPushNotification(userId, payload)) ); const successful = results.filter((r) => r.status === 'fulfilled').length; return { success: true, notified: successful, total: userIds.length }; } catch (error) { console.error('Failed to notify list collaborators:', error); return { success: false, error: 'Failed to send notifications' }; } } /** * Get VAPID public key for client-side subscription */ export async function getVapidPublicKey() { return process.env.VAPID_PUBLIC_KEY || null; }