/** * Notification REST API — mounted at /api/notifications * * All endpoints require Bearer auth (same pattern as spaces.ts). */ import { Hono } from "hono"; import { verifyEncryptIDToken, extractToken, } from "@encryptid/sdk/server"; import { getUserNotifications, getUnreadCount, markNotificationRead, markAllNotificationsRead, dismissNotification, getNotificationPreferences, upsertNotificationPreferences, } from "../src/encryptid/db"; export const notificationRouter = new Hono(); // ── Auth helper ── async function requireAuth(req: Request) { const token = extractToken(req.headers); if (!token) return null; try { return await verifyEncryptIDToken(token); } catch { return null; } } // ── GET / — Paginated notification list ── notificationRouter.get("/", async (c) => { const claims = await requireAuth(c.req.raw); if (!claims) return c.json({ error: "Authentication required" }, 401); const url = new URL(c.req.url); const unreadOnly = url.searchParams.get("unread") === "true"; const category = url.searchParams.get("category") || undefined; const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 100); const offset = Number(url.searchParams.get("offset")) || 0; const notifications = await getUserNotifications(claims.sub, { unreadOnly, category, limit, offset, }); return c.json({ notifications }); }); // ── GET /count — Lightweight unread count (polling fallback) ── notificationRouter.get("/count", async (c) => { const claims = await requireAuth(c.req.raw); if (!claims) return c.json({ error: "Authentication required" }, 401); const count = await getUnreadCount(claims.sub); return c.json({ unreadCount: count }); }); // ── PATCH /:id/read — Mark one notification as read ── notificationRouter.patch("/:id/read", async (c) => { const claims = await requireAuth(c.req.raw); if (!claims) return c.json({ error: "Authentication required" }, 401); const id = c.req.param("id"); const ok = await markNotificationRead(id, claims.sub); if (!ok) return c.json({ error: "Notification not found" }, 404); return c.json({ ok: true }); }); // ── POST /read-all — Mark all read (optional scope) ── notificationRouter.post("/read-all", async (c) => { const claims = await requireAuth(c.req.raw); if (!claims) return c.json({ error: "Authentication required" }, 401); let spaceSlug: string | undefined; let category: string | undefined; try { const body = await c.req.json(); spaceSlug = body.spaceSlug; category = body.category; } catch { // No body is fine — mark everything read } const count = await markAllNotificationsRead(claims.sub, { spaceSlug, category }); return c.json({ ok: true, markedRead: count }); }); // ── DELETE /:id — Dismiss/archive a notification ── notificationRouter.delete("/:id", async (c) => { const claims = await requireAuth(c.req.raw); if (!claims) return c.json({ error: "Authentication required" }, 401); const id = c.req.param("id"); const ok = await dismissNotification(id, claims.sub); if (!ok) return c.json({ error: "Notification not found" }, 404); return c.json({ ok: true }); }); // ── GET /preferences — Get notification preferences ── notificationRouter.get("/preferences", async (c) => { const claims = await requireAuth(c.req.raw); if (!claims) return c.json({ error: "Authentication required" }, 401); const prefs = await getNotificationPreferences(claims.sub); return c.json({ preferences: prefs || { emailEnabled: true, pushEnabled: true, quietHoursStart: null, quietHoursEnd: null, mutedSpaces: [], mutedCategories: [], digestFrequency: 'none', }}); }); // ── PATCH /preferences — Update notification preferences ── notificationRouter.patch("/preferences", async (c) => { const claims = await requireAuth(c.req.raw); if (!claims) return c.json({ error: "Authentication required" }, 401); const body = await c.req.json(); const prefs = await upsertNotificationPreferences(claims.sub, body); return c.json({ preferences: prefs }); });