phase 8: site-access and admin simplified and compacted
This commit is contained in:
90
api/src/routes/site-access.ts
Normal file
90
api/src/routes/site-access.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { FastifyPluginAsync } from "fastify";
|
||||
import { z } from "zod";
|
||||
|
||||
type RateLimitRouteOptions = {
|
||||
config: {
|
||||
rateLimit: {
|
||||
max: number;
|
||||
timeWindow: number;
|
||||
keyGenerator?: (req: any) => string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type SiteAccessRoutesOptions = {
|
||||
underConstructionEnabled: boolean;
|
||||
breakGlassVerifyEnabled: boolean;
|
||||
breakGlassVerifyCode: string | null;
|
||||
siteAccessExpectedToken: string | null;
|
||||
cookieDomain?: string;
|
||||
secureCookie: boolean;
|
||||
siteAccessCookieName: string;
|
||||
siteAccessMaxAgeSeconds: number;
|
||||
authRateLimit: RateLimitRouteOptions;
|
||||
mutationRateLimit: RateLimitRouteOptions;
|
||||
hasSiteAccessBypass: (req: { cookies?: Record<string, unknown> }) => boolean;
|
||||
safeEqual: (a: string, b: string) => boolean;
|
||||
};
|
||||
|
||||
const siteAccessRoutes: FastifyPluginAsync<SiteAccessRoutesOptions> = async (app, opts) => {
|
||||
app.get("/site-access/status", async (req) => {
|
||||
if (!opts.underConstructionEnabled) {
|
||||
return { ok: true, enabled: false, unlocked: true };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
enabled: true,
|
||||
unlocked: opts.hasSiteAccessBypass(req),
|
||||
};
|
||||
});
|
||||
|
||||
app.post("/site-access/unlock", opts.authRateLimit, async (req, reply) => {
|
||||
const Body = z.object({
|
||||
code: z.string().min(1).max(512),
|
||||
});
|
||||
const parsed = Body.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({ ok: false, code: "INVALID_PAYLOAD", message: "Invalid payload" });
|
||||
}
|
||||
if (!opts.underConstructionEnabled) {
|
||||
return { ok: true, enabled: false, unlocked: true };
|
||||
}
|
||||
if (!opts.breakGlassVerifyEnabled || !opts.siteAccessExpectedToken) {
|
||||
return reply.code(503).send({
|
||||
ok: false,
|
||||
code: "UNDER_CONSTRUCTION_MISCONFIGURED",
|
||||
message: "Under-construction access is not configured.",
|
||||
});
|
||||
}
|
||||
if (!opts.breakGlassVerifyCode || !opts.safeEqual(parsed.data.code, opts.breakGlassVerifyCode)) {
|
||||
return reply.code(401).send({
|
||||
ok: false,
|
||||
code: "INVALID_ACCESS_CODE",
|
||||
message: "Invalid access code.",
|
||||
});
|
||||
}
|
||||
|
||||
reply.setCookie(opts.siteAccessCookieName, opts.siteAccessExpectedToken, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: opts.secureCookie,
|
||||
path: "/",
|
||||
maxAge: opts.siteAccessMaxAgeSeconds,
|
||||
...(opts.cookieDomain ? { domain: opts.cookieDomain } : {}),
|
||||
});
|
||||
return { ok: true, enabled: true, unlocked: true };
|
||||
});
|
||||
|
||||
app.post("/site-access/lock", opts.mutationRateLimit, async (_req, reply) => {
|
||||
reply.clearCookie(opts.siteAccessCookieName, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: opts.secureCookie,
|
||||
...(opts.cookieDomain ? { domain: opts.cookieDomain } : {}),
|
||||
});
|
||||
return { ok: true, enabled: opts.underConstructionEnabled, unlocked: false };
|
||||
});
|
||||
};
|
||||
|
||||
export default siteAccessRoutes;
|
||||
Reference in New Issue
Block a user