import webpush from 'web-push'; import { storage } from '../storage'; const VAPID_KEY_CONFIG_KEY = 'vapid_public_key'; interface WebPushSubscription { endpoint: string; keys: { p256dh: string; auth: string; }; } class WebPushService { private initialized = false; private vapidPublicKey: string = ''; private vapidPrivateKey: string = ''; constructor() { this.initialize(); } private initialize() { const publicKey = process.env.VAPID_PUBLIC_KEY; const privateKey = process.env.VAPID_PRIVATE_KEY; if (!publicKey || !privateKey) { console.warn('[WebPush] WARNING: VAPID_PUBLIC_KEY and/or VAPID_PRIVATE_KEY are not set.'); console.warn('[WebPush] Generating temporary VAPID keys for this session only.'); console.warn('[WebPush] All existing push subscriptions will be invalidated on every server restart!'); console.warn('[WebPush] To fix this, generate permanent keys and set them as environment variables:'); console.warn('[WebPush] npx web-push generate-vapid-keys'); console.warn('[WebPush] Then set VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY in your environment secrets.'); const keys = webpush.generateVAPIDKeys(); this.vapidPublicKey = keys.publicKey; this.vapidPrivateKey = keys.privateKey; } else { this.vapidPublicKey = publicKey; this.vapidPrivateKey = privateKey; } webpush.setVapidDetails( 'mailto:admin@iistwin.ru', this.vapidPublicKey, this.vapidPrivateKey ); this.initialized = true; console.log('[WebPush] Service initialized'); } async checkVapidKeyRotation(): Promise { try { const storedKey = await storage.getSystemConfig(VAPID_KEY_CONFIG_KEY); if (storedKey === null) { await storage.setSystemConfig(VAPID_KEY_CONFIG_KEY, this.vapidPublicKey); console.log('[WebPush] VAPID public key stored in system config for future rotation checks.'); return; } if (storedKey !== this.vapidPublicKey) { console.warn('[WebPush] VAPID public key has changed! Previous key does not match current key.'); console.warn('[WebPush] Clearing all existing web push subscriptions — they are no longer valid.'); const count = await storage.clearAllWebPushSubscriptions(); console.warn(`[WebPush] Removed ${count} stale push subscription(s).`); await storage.setSystemConfig(VAPID_KEY_CONFIG_KEY, this.vapidPublicKey); console.log('[WebPush] System config updated with new VAPID public key.'); } } catch (err) { console.error('[WebPush] Error during VAPID key rotation check:', err); } } getVapidPublicKey(): string { return this.vapidPublicKey; } async registerSubscription( userId: number, organizationId: number, subscription: WebPushSubscription ): Promise { await storage.registerWebPushSubscription(userId, organizationId, subscription.endpoint, JSON.stringify(subscription)); console.log(`[WebPush] Subscription registered for user ${userId}`); } async unregisterSubscription(endpoint: string): Promise { await storage.unregisterWebPushSubscription(endpoint); console.log(`[WebPush] Subscription unregistered: ${endpoint.substring(0, 50)}...`); } async sendNotification( subscription: WebPushSubscription, payload: { title?: string; body?: string; icon?: string; url?: string; tag?: string; data?: Record; type?: string; [key: string]: any; } ): Promise { if (!this.initialized) { console.log('[WebPush] Service not initialized'); return false; } try { const result = await webpush.sendNotification( subscription, JSON.stringify(payload) ); console.log(`[WebPush] FCM response status=${result.statusCode}, body=${result.body?.substring?.(0, 200) ?? result.body}`); return true; } catch (error: any) { if (error.statusCode === 410 || error.statusCode === 404) { console.log('[WebPush] Subscription expired, removing...'); await this.unregisterSubscription(subscription.endpoint); } else { console.error('[WebPush] Send error:', error.message); } return false; } } async sendToUser( userId: number, organizationId: number, payload: { title?: string; body?: string; icon?: string; url?: string; tag?: string; data?: Record; type?: string; [key: string]: any; } ): Promise { const subscriptions = await storage.getWebPushSubscriptions(userId, organizationId); let sent = 0; for (const sub of subscriptions) { try { const subscription = JSON.parse(sub.subscription); const success = await this.sendNotification(subscription, payload); if (success) sent++; } catch (error) { console.error('[WebPush] Parse error for subscription:', error); } } return sent; } } export const webPushService = new WebPushService();