SPB Git

Repository Public

authroutes.ts 2.9 KB 51 lines typescript

1import { z } from "zod";2import { ApiError, body, clientIp } from "../http";3import { rateLimit } from "../ratelimit";4import {5  assertSameOrigin, authenticationOptions, clearSessionCookie, currentAdmin, loginWithRecoveryCode, registrationOptions, revokeSession,6  verifyAuthentication, verifyRegistration,7} from "../auth";8import { audit } from "../audit";9import type { RouteDef } from "./router";1011const anyJson = z.record(z.string(), z.unknown());1213export const authRoutes: RouteDef<{ ip: string }>[] = [14  ["GET", "session", async () => {15    const a = await currentAdmin();16    return a ? { authenticated: true, csrfToken: a.csrfToken, user: { email: a.email, displayName: a.displayName } } : { authenticated: false };17  }],18  ["POST", "register/options", async ({ req, ctx }) => {19    assertSameOrigin(req); await rateLimit(`auth:${ctx.ip}`, 20, 600);20    const b = await body(req, z.object({ setupToken: z.string().max(200).optional() }));21    const admin = await currentAdmin();22    if (admin) { const sent = req.headers.get("x-csrf-token") ?? ""; if (sent !== admin.csrfToken) throw new ApiError(403, "csrf", "Invalid CSRF token"); }23    return registrationOptions({ setupToken: b.setupToken, admin: b.setupToken ? null : admin });24  }],25  ["POST", "register/verify", async ({ req, ctx }) => {26    assertSameOrigin(req); await rateLimit(`auth:${ctx.ip}`, 20, 600);27    const b = await body(req, z.object({ challengeId: z.string().uuid(), response: anyJson, name: z.string().max(60).optional(), setupToken: z.string().max(200).optional() }));28    const admin = b.setupToken ? null : await currentAdmin();29    if (admin) { const sent = req.headers.get("x-csrf-token") ?? ""; if (sent !== admin.csrfToken) throw new ApiError(403, "csrf", "Invalid CSRF token"); }30    return verifyRegistration({ challengeId: b.challengeId, response: b.response as never, name: b.name, setupToken: b.setupToken, admin }, req);31  }],32  ["POST", "login/options", async ({ req, ctx }) => { assertSameOrigin(req); await rateLimit(`auth:${ctx.ip}`, 30, 600); return authenticationOptions(); }],33  ["POST", "login/verify", async ({ req, ctx }) => {34    assertSameOrigin(req); await rateLimit(`auth:${ctx.ip}`, 30, 600);35    const b = await body(req, z.object({ challengeId: z.string().uuid(), response: anyJson }));36    return verifyAuthentication({ challengeId: b.challengeId, response: b.response as never }, req);37  }],38  ["POST", "recovery", async ({ req, ctx }) => {39    assertSameOrigin(req); await rateLimit(`recovery:${ctx.ip}`, 5, 900);40    const b = await body(req, z.object({ code: z.string().min(8).max(40) }));41    return loginWithRecoveryCode(b.code, req);42  }],43  ["POST", "logout", async ({ req }) => {44    assertSameOrigin(req);45    const a = await currentAdmin();46    if (a) { await revokeSession(a.sessionId, a.userId); await audit({ userId: a.userId, actor: "admin", action: "logout", ip: clientIp(req) }); }47    await clearSessionCookie();48    return { ok: true };49  }],50];51
Files main
Menu

Theme