sitegen/src/admin.ts
2025-08-02 20:56:36 -04:00

75 lines
2.1 KiB
TypeScript

const cookieAge = 60 * 60 * 24 * 365; // 1 year
let lastKnownToken: string | null = null;
function compareToken(token: string) {
if (token === lastKnownToken) return true;
lastKnownToken = fs.readFileSync(".clover/admin-token.txt", "utf8").trim();
return token === lastKnownToken;
}
export async function middleware(c: Context, next: Next) {
if (c.req.path.startsWith("/admin")) {
return adminInner(c, next);
}
return next();
}
export function adminInner(c: Context, next: Next) {
const token = c.req.header("Cookie")?.match(/admin-token=([^;]+)/)?.[1];
if (c.req.path === "/admin/login") {
const key = c.req.query("key");
if (key) {
if (compareToken(key)) {
return c.body(null, 303, {
"Location": "/admin",
"Set-Cookie":
`admin-token=${key}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${cookieAge}`,
});
}
return serveAsset(c, "/admin/login/fail", 403);
}
if (token && compareToken(token)) {
return c.redirect("/admin", 303);
}
if (c.req.method === "POST") {
return serveAsset(c, "/admin/login/fail", 403);
} else {
return serveAsset(c, "/admin/login", 200);
}
}
if (c.req.path === "/admin/logout") {
return c.body(null, 303, {
"Location": "/admin/login",
"Set-Cookie":
`admin-token=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`,
});
}
if (token && compareToken(token)) {
return next();
}
return c.redirect("/admin/login", 303);
}
export function hasAdminToken(c: Context) {
const token = c.req.header("Cookie")?.match(/admin-token=([^;]+)/)?.[1];
return token && compareToken(token);
}
export async function main() {
const key = crypto.randomUUID();
await fs.writeMkdir(".clover/admin-token.txt", key);
const start = ({
win32: "start",
darwin: "open",
} as Record<string, string>)[process.platform] ?? "xdg-open";
child_process.exec(`${start} http://[::1]:3000/admin/login?key=${key}`);
}
import * as fs from "#sitegen/fs";
import type { Context, Next } from "hono";
import { serveAsset } from "#sitegen/assets";
import * as child_process from "node:child_process";