1const cookieAge = 60 * 60 * 24 * 365; // 1 year
2
3let lastKnownToken: string | null = null;
4function compareToken(token: string) {
5 if (token === lastKnownToken) return true;
6 try {
7 lastKnownToken = fs.readFileSync(".clover/admin-token.txt", "utf8").trim();
8 } catch {
9 return false;
10 }
11 return token === lastKnownToken;
12}
13
14export async function middleware(c: Context, next: Next) {
15 if (c.req.path.startsWith("/admin")) return adminInner(c, next);
16 return next();
17}
18
19export function adminInner(c: Context, next: Next) {
20 const token = c.req.header("Cookie")?.match(/admin-token=([^;]+)/)?.[1];
21
22 if (c.req.path === "/admin/login") {
23 const key = c.req.query("key");
24 if (key) {
25 if (compareToken(key)) {
26 return c.body(null, 303, {
27 "Location": "/admin",
28 "Set-Cookie": `admin-token=${key}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${cookieAge}`,
29 });
30 }
31 return serveAsset(c, "/admin/login/fail", 403);
32 }
33 if (token && compareToken(token)) {
34 return c.redirect("/admin", 303);
35 }
36 if (c.req.method === "POST") {
37 return serveAsset(c, "/admin/login/fail", 403);
38 } else {
39 return serveAsset(c, "/admin/login", 200);
40 }
41 }
42
43 if (c.req.path === "/admin/logout") {
44 return c.body(null, 303, {
45 "Location": "/admin/login",
46 "Set-Cookie": `admin-token=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`,
47 });
48 }
49
50 if (token && compareToken(token)) {
51 return next();
52 }
53
54 return c.redirect("/admin/login", 303);
55}
56
57export function hasAdminToken(c: Context) {
58 const token = c.req.header("Cookie")?.match(/admin-token=([^;]+)/)?.[1];
59 return token && compareToken(token);
60}
61
62export async function main() {
63 const key = crypto.randomUUID();
64 await fs.writeMkdir(".clover/admin-token.txt", key);
65 const start = ({
66 win32: "start",
67 darwin: "open",
68 } as Record<string, string>)[process.platform] ?? "xdg-open";
69 child_process.exec(`${start} http://[::1]:3000/admin/login?key=${key}`);
70}
71
72import { serveAsset } from "#sitegen/assets";
73import * as fs from "#sitegen/fs";
74import type { Context, Next } from "hono";
75import * as child_process from "node:child_process";