132 lines
4.1 KiB
TypeScript
132 lines
4.1 KiB
TypeScript
/**
|
|
* 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 });
|
|
});
|