import type { FastifyPluginAsync } from "fastify"; import { randomUUID } from "node:crypto"; import { z } from "zod"; import type { AppConfig } from "../server.js"; type SessionRoutesOptions = { config: Pick< AppConfig, "NODE_ENV" | "UPDATE_NOTICE_VERSION" | "UPDATE_NOTICE_TITLE" | "UPDATE_NOTICE_BODY" >; cookieDomain?: string; mutationRateLimit: { config: { rateLimit: { max: number; timeWindow: number; }; }; }; }; const CSRF_COOKIE = "csrf"; const sessionRoutes: FastifyPluginAsync = async ( app, opts ) => { const ensureCsrfCookie = (reply: any, existing?: string) => { const token = existing ?? randomUUID().replace(/-/g, ""); reply.setCookie(CSRF_COOKIE, token, { httpOnly: false, sameSite: "lax", secure: opts.config.NODE_ENV === "production", path: "/", ...(opts.cookieDomain ? { domain: opts.cookieDomain } : {}), }); return token; }; app.get("/auth/session", async (req, reply) => { if (!(req.cookies as any)?.[CSRF_COOKIE]) { ensureCsrfCookie(reply); } const user = await app.prisma.user.findUnique({ where: { id: req.userId }, select: { email: true, displayName: true, emailVerified: true, seenUpdateVersion: true, }, }); const noticeVersion = opts.config.UPDATE_NOTICE_VERSION; const shouldShowNotice = noticeVersion > 0 && !!user && user.emailVerified && user.seenUpdateVersion < noticeVersion; return { ok: true, userId: req.userId, email: user?.email ?? null, displayName: user?.displayName ?? null, emailVerified: user?.emailVerified ?? false, updateNotice: shouldShowNotice ? { version: noticeVersion, title: opts.config.UPDATE_NOTICE_TITLE, body: opts.config.UPDATE_NOTICE_BODY, } : null, }; }); app.post( "/app/update-notice/ack", opts.mutationRateLimit, async (req, reply) => { const Body = z.object({ version: z.coerce.number().int().nonnegative().optional(), }); const parsed = Body.safeParse(req.body); if (!parsed.success) { return reply.code(400).send({ ok: false, message: "Invalid payload" }); } const targetVersion = parsed.data.version ?? opts.config.UPDATE_NOTICE_VERSION; await app.prisma.user.updateMany({ where: { id: req.userId, seenUpdateVersion: { lt: targetVersion }, }, data: { seenUpdateVersion: targetVersion }, }); return { ok: true }; } ); }; export default sessionRoutes;