diff --git a/framework/esbuild-support.ts b/framework/esbuild-support.ts index c8e7520bbd94ee3a7ce17c3af3af92a515256ecd..c90df2b3d944b35c466beb5bdbe896ed34727926 100644 --- a/framework/esbuild-support.ts +++ b/framework/esbuild-support.ts @@ -64,7 +64,7 @@ export function markoViaBuildCache(): esbuild.Plugin { name: "marko via build cache", setup(b) { b.onLoad( - { filter: /\.marko$/ }, + { filter: /\.marko$|\.mdo$/ }, async ({ path: file }) => { const cacheEntry = markoCache.get(file); if (!cacheEntry) { diff --git a/framework/lib/view.ts b/framework/lib/view.ts index a324214c53bbcaa71f47911a6c0a079cc0fa41c2..c8ad37589db256ce302156b8685215fcb33d5bcf 100644 --- a/framework/lib/view.ts +++ b/framework/lib/view.ts @@ -6,7 +6,8 @@ let codegen: Codegen; try { codegen = require("$views"); -} catch { +} catch (e) { + console.error(e); throw new Error("Can only import '#sitegen/view' in backends."); } diff --git a/src/friend-auth.ts b/src/friend-auth.ts index 1f1c53aa07f52b402e2ca0b1dae2af48d2bbacee..727a6c077d55d69b4d09170a9316347c0af05e1d 100644 --- a/src/friend-auth.ts +++ b/src/friend-auth.ts @@ -1,12 +1,12 @@ let hardcoded = { friendPassword: "", - perPage: {} as Record, + monthlyPasswords: [] as Array<{ password: string; start: string }>, getForFile: (_: string) => [] as string[], }; try { hardcoded = require("./friends/hardcoded-password.ts"); } catch {} -hardcoded.perPage ??= {}; +hardcoded.monthlyPasswords ??= []; export const app = new Hono(); @@ -14,16 +14,17 @@ const cookieAge = 60 * 60 * 24 * 30; // 1 month export const getForFile = hardcoded.getForFile ?? (() => []); -function checkFriendsCookie(c: Context, passwords: string[]) { +function getFriendsCookiePassword(c: Context, passwords: string[]) { const cookie = c.req.header("Cookie"); - if (!cookie) return false; + if (!cookie) return null; const cookies = cookie.split("; ").map((x) => x.split("=")); - return cookies.some( - (kv) => - kv[0]!.trim() === "friends_password" - && kv[1]!.trim() - && passwords.includes(kv[1]!.trim()), - ); + for (const kv of cookies) { + const password = kv[1]?.trim(); + if (kv[0]!.trim() === "friends_password" && password && passwords.includes(password)) { + return password; + } + } + return null; } export function requireFriendAuth( @@ -46,7 +47,7 @@ export function requireFriendAuth( }); } } - if (checkFriendsCookie(c, passwords)) { + if (getFriendsCookiePassword(c, passwords)) { return undefined; } else { return serveAsset(c, "/friends/auth", 403); @@ -55,39 +56,81 @@ export function requireFriendAuth( let incorrectMap: Record = {}; -function friendPage(route: assets.Key) { - const expected = hardcoded.perPage[route] ?? hardcoded.friendPassword; +app.use(friendAuthMiddleware); - app.get(route, (c) => { - const friendAuthChallenge = requireFriendAuth(c, [expected]); +app.get("/friends", (c) => { + return view.serve(c, "friends/index", { + minimumDate: getMinimumDate(c), + }); +}); + +async function friendAuthMiddleware(c: Context, next: Next) { + if (isFriendAuthPage(c.req.path)) return next(); + if (!isFriendRoute(c.req.path)) return next(); + + const passwords = getForRoute(c.req.path); + if (c.req.method !== "POST") { + const friendAuthChallenge = requireFriendAuth(c, passwords); if (friendAuthChallenge) return friendAuthChallenge; - return serveAsset(c, route, 200); - }); + return next(); + } - app.post(route, async (c) => { - const ip = c.header("X-Forwarded-For") ?? "unknown"; - if (incorrectMap[ip]) { - return serveAsset(c, "/friends/auth/fail", 403); - } - const data = await c.req.formData(); - const k = data.get("password"); - if (k === expected) { - return c.body(null, 303, { - Location: c.req.path, - "Set-Cookie": `friends_password=${k}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${cookieAge}`, - }); - } - incorrectMap[ip] = true; - await setTimeout(2500); - incorrectMap[ip] = false; + const ip = c.header("X-Forwarded-For") ?? "unknown"; + if (incorrectMap[ip]) { return serveAsset(c, "/friends/auth/fail", 403); - }); + } + const data = await c.req.formData(); + const k = data.get("password"); + if (typeof k === "string" && passwords.includes(k)) { + return c.body(null, 303, { + Location: c.req.path, + "Set-Cookie": `friends_password=${k}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${cookieAge}`, + }); + } + incorrectMap[ip] = true; + await setTimeout(2500); + incorrectMap[ip] = false; + return serveAsset(c, "/friends/auth/fail", 403); } -friendPage("/friends"); -friendPage("/friends/oct25"); +function getForRoute(route: string) { + const passwords = [hardcoded.friendPassword]; + const month = getRouteMonth(route); + if (route === "/friends") { + passwords.push(...hardcoded.monthlyPasswords.map((grant) => grant.password)); + return passwords; + } + if (!month) return passwords; + for (const grant of hardcoded.monthlyPasswords) { + if (month >= grant.start) passwords.push(grant.password); + } + return passwords; +} + +function getMinimumDate(c: Context) { + const password = getFriendsCookiePassword(c, getForRoute(c.req.path)); + if (!password || password === hardcoded.friendPassword) return null; + const grant = hardcoded.monthlyPasswords.find((grant) => grant.password === password); + return grant?.start ?? null; +} + +function getRouteMonth(route: string) { + const [, year, month] = route.match(/^(?:\/friends)?\/(\d\d)\/(\d\d)-/) ?? []; + if (!year || !month) return null; + return `20${year}-${month}`; +} + +function isFriendAuthPage(route: string) { + return route === "/friends/auth" + || route === "/friends/auth/fail" + || route === "/friends/misconfigure"; +} + +function isFriendRoute(route: string) { + return route === "/friends" || getRouteMonth(route) !== null; +} import { serveAsset } from "#sitegen/assets"; -import * as assets from "#sitegen/assets"; -import { type Context, Hono } from "hono"; +import * as view from "#sitegen/view"; +import { type Context, Hono, type Next } from "hono"; import { setTimeout } from "node:timers/promises"; diff --git a/src/pages/dream.mdo b/src/pages/dream.mdo index 6b7981d9a728e4f924f355cc3e40738f137e9af5..69f3591933d804abc22d6edf1904db82686caa00 100644 --- a/src/pages/dream.mdo +++ b/src/pages/dream.mdo @@ -1,6 +1,6 @@ --- meta: - title: paper clover's media license + title: clo's dream ---
diff --git a/src/site.ts b/src/site.ts index a0ce9dbab760ac8982e2f7be01e395f179408765..54ddbb72062c0b625b9713eff363adb8d791e55f 100644 --- a/src/site.ts +++ b/src/site.ts @@ -9,7 +9,7 @@ export const siteSections: sg.Section[] = [ { root: join(".") }, { root: join("q+a/") }, { root: join("file-viewer/") }, - { root: join("friends/") }, + { root: join("friends/"), base: "/friends" }, { root: join("blog/"), base: "/blog" }, // { root: join("fiction/"), pageBase: "/fiction" }, ];