/** * Push subscription endpoints. * * POST /api/push/subscribe — register a new browser push endpoint * DELETE /api/push/unsubscribe — remove a subscription by endpoint * * Subscriptions are stored in push_subscriptions (migration 015) with RLS * gated to auth.uid() = user_id. We use the service role here so we don't * have to thread the user JWT through Supabase — requireAuth has already * verified the user. */ const express = require('express'); const { requireAuth } = require('../middleware/auth'); const { getSupabaseServiceClient } = require('../utils/supabase'); const router = express.Router(); function validSubscription(sub) { if (!sub || typeof sub !== 'object') return false; if (typeof sub.endpoint !== 'string' || !sub.endpoint.startsWith('https://')) return false; if (!sub.keys || typeof sub.keys !== 'object') return false; if (typeof sub.keys.p256dh !== 'string' || typeof sub.keys.auth !== 'string') return false; return true; } router.post('/subscribe', requireAuth, async (req, res) => { const { subscription, preferences } = req.body || {}; if (!validSubscription(subscription)) { return res.status(400).json({ error: 'Invalid subscription payload' }); } try { const supabase = getSupabaseServiceClient(); const row = { user_id: req.user.id, endpoint: subscription.endpoint, keys_p256dh: subscription.keys.p256dh, keys_auth: subscription.keys.auth, }; if (Array.isArray(preferences?.sports)) row.sport_preferences = preferences.sports; if (typeof preferences?.notify_on_resolution === 'boolean') { row.notify_on_resolution = preferences.notify_on_resolution; } if (typeof preferences?.notify_on_cascade === 'boolean') { row.notify_on_cascade = preferences.notify_on_cascade; } if (typeof preferences?.notify_on_cheatsheet === 'boolean') { row.notify_on_cheatsheet = preferences.notify_on_cheatsheet; } const { error } = await supabase .from('push_subscriptions') .upsert(row, { onConflict: 'user_id,endpoint' }); if (error) { console.error('[VYNDR] Push subscribe error:', error.message); return res.status(503).json({ error: 'Subscription save failed' }); } return res.json({ ok: true }); } catch (err) { console.error('[VYNDR] Push subscribe error:', err.message); return res.status(503).json({ error: 'Subscription save failed' }); } }); router.delete('/unsubscribe', requireAuth, async (req, res) => { const { endpoint } = req.body || {}; if (typeof endpoint !== 'string' || !endpoint.startsWith('https://')) { return res.status(400).json({ error: 'Invalid endpoint' }); } try { const supabase = getSupabaseServiceClient(); const { error } = await supabase .from('push_subscriptions') .delete() .eq('user_id', req.user.id) .eq('endpoint', endpoint); if (error) { console.error('[VYNDR] Push unsubscribe error:', error.message); return res.status(503).json({ error: 'Unsubscribe failed' }); } return res.json({ ok: true }); } catch (err) { console.error('[VYNDR] Push unsubscribe error:', err.message); return res.status(503).json({ error: 'Unsubscribe failed' }); } }); module.exports = router;