All files / src/routes suggestions-notifications.js

100% Statements 40/40
100% Branches 10/10
100% Functions 7/7
100% Lines 34/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86                1x 1x 1x     16x 3x 3x   13x         1x 8x 8x   7x           12x 12x   6x   1x 1x               1x 4x 4x   3x           3x 3x 3x   2x   1x 1x           1x 4x 4x   3x   2x   1x 1x           1x  
/**
 * Suggestion notification inbox routes.
 *
 * GET  /notifications          -> paginated inbox, newest first, with unreadCount
 * PUT  /notifications/read-all -> mark all notifications as read
 * PUT  /notifications/:id/read -> mark single notification as read
 */
 
const router = require('express').Router();
const { db } = require('../utils/firebase');
const log = require('../utils/log');
 
function requireAuth(req, res) {
  if (!req.auth || !req.auth.uniqueId) {
    res.status(401).json({ error: 'Authentication required' });
    return true;
  }
  return false;
}
 
// ─── GET /notifications ─────────────────────────────────────────
 
router.get('/notifications', async (req, res) => {
  try {
    if (requireAuth(req, res)) return;
 
    const snap = await db
      .collection('notifications')
      .where('uid', '==', req.auth.uniqueId)
      .orderBy('createdAt', 'desc')
      .get();
 
    const notifications = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
    const unreadCount = notifications.filter((n) => !n.isRead).length;
 
    res.json({ notifications, unreadCount, total: notifications.length });
  } catch (err) {
    log.error('notifications', 'Failed to list', { error: err.message });
    res.status(500).json({ error: 'Internal server error' });
  }
});
 
// ─── PUT /notifications/read-all ────────────────────────────────
// NOTE: Must be registered BEFORE /:id/read to avoid "read-all"
// being captured as an :id parameter.
 
router.put('/notifications/read-all', async (req, res) => {
  try {
    if (requireAuth(req, res)) return;
 
    const snap = await db
      .collection('notifications')
      .where('uid', '==', req.auth.uniqueId)
      .where('isRead', '==', false)
      .get();
 
    const batch = db.batch();
    snap.docs.forEach((d) => batch.update(db.doc('notifications/' + d.id), { isRead: true }));
    await batch.commit();
 
    res.json({ success: true, updated: snap.size });
  } catch (err) {
    log.error('notifications', 'Mark all read failed', { error: err.message });
    res.status(500).json({ error: 'Internal server error' });
  }
});
 
// ─── PUT /notifications/:id/read ────────────────────────────────
 
router.put('/notifications/:id/read', async (req, res) => {
  try {
    if (requireAuth(req, res)) return;
 
    await db.doc('notifications/' + req.params.id).update({ isRead: true });
 
    res.json({ success: true });
  } catch (err) {
    log.error('notifications', 'Mark read failed', { error: err.message });
    res.status(500).json({ error: 'Internal server error' });
  }
});
 
// POST /subscriptions/unsubscribe is handled by subscriptions.js (HMAC-based token verification)
 
module.exports = router;