// Routes Web Push (P4). Toutes sous l'auth globale (preValidation) : aucune route publique. // L'abonnement est lié au token authentifié (req.authContext.tokenId). import type { FastifyInstance } from 'fastify'; import type { PushSubscribeRequest, PushUnsubscribeRequest, VapidKeyResponse } from '@arboretum/shared'; import type { PushService } from '../core/push-service.js'; import type { Db } from '../db/index.js'; import { recordAudit } from '../core/audit-log.js'; export function registerPushRoutes(app: FastifyInstance, push: PushService, db: Db): void { app.get('/api/v1/push/vapid-public-key', async (): Promise => ({ key: push.publicKey() })); app.post('/api/v1/push/subscribe', async (req, reply) => { const body = req.body as Partial | null; if (!body || typeof body.endpoint !== 'string' || !body.keys || typeof body.keys.p256dh !== 'string' || typeof body.keys.auth !== 'string') { return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'endpoint and keys{p256dh,auth} are required' } }); } // garanti non-null : la route est protégée par le preValidation global. const tokenId = req.authContext!.tokenId; push.subscribe(tokenId, { endpoint: body.endpoint, keys: { p256dh: body.keys.p256dh, auth: body.keys.auth } }, req.headers['user-agent'] ?? null); recordAudit(db, { actor: tokenId, action: 'push.subscribe' }); return reply.status(201).send({ ok: true }); }); app.post('/api/v1/push/unsubscribe', async (req, reply) => { const body = req.body as Partial | null; if (!body || typeof body.endpoint !== 'string') { return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'endpoint is required' } }); } push.unsubscribe(body.endpoint); recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'push.unsubscribe' }); return reply.send({ ok: true }); }); // Notification de test (diagnostic mobile) : pousse vers tous les abonnements de l'utilisateur. app.post('/api/v1/push/test', async (_req, reply) => { await push.notify({ sessionId: 'test', title: 'Arboretum', body: 'Push notifications are working.', kind: null, url: '/' }); return reply.send({ ok: true }); }); }