import { Router } from "express"; import { checkOwnerCredentials, clearSessionCookie, createSessionCookie, requireOwnerWeb, throttleKey, tooManyAttempts, } from "../auth.ts"; import { config } from "../config.ts"; import { createRepo, deleteRepo, listRepos, updateRepo } from "../repos.ts"; import { recordFailure, recordSuccess } from "../throttle.ts"; import { adminPage, layout, loginPage } from "../views.ts"; export const adminRouter: Router = Router(); /** Alleen paden binnen deze site zijn een geldige redirect na inloggen. */ function safeNext(value: unknown): string { if (typeof value !== "string") return "/admin"; if (!value.startsWith("/") || value.startsWith("//")) return "/admin"; return value; } function backToAdmin(message: string | null, error: string | null): string { const params = new URLSearchParams(); if (message) params.set("ok", message); if (error) params.set("error", error); const query = params.toString(); return query ? `/admin?${query}` : "/admin"; } /** * De sessiecookie is SameSite=Lax, dus een andere site kan geen POST met cookie * versturen. Deze controle vangt de rest af, inclusief formulieren op subdomeinen. */ adminRouter.use((req, res, next) => { if (req.method !== "POST") return next(); const origin = req.get("origin"); if (origin && origin !== config.publicUrl && !origin.startsWith("http://localhost")) { res.status(403).type("text/plain").send("Ongeldige herkomst.\n"); return; } next(); }); adminRouter.get("/login", (req, res) => { if (req.isOwner) { res.redirect(safeNext(req.query["next"])); return; } res.type("html").send( layout({ title: "Inloggen", isOwner: false, body: loginPage(safeNext(req.query["next"]), null), }), ); }); adminRouter.post("/login", (req, res) => { if (tooManyAttempts(res, throttleKey(req))) return; const user = typeof req.body?.user === "string" ? req.body.user : ""; const password = typeof req.body?.password === "string" ? req.body.password : ""; const next = safeNext(req.body?.next); if (!checkOwnerCredentials(user, password)) { recordFailure(throttleKey(req)); res .status(401) .type("html") .send( layout({ title: "Inloggen", isOwner: false, body: loginPage(next, "Onjuiste gebruikersnaam of wachtwoord."), }), ); return; } recordSuccess(throttleKey(req)); res.setHeader("Set-Cookie", createSessionCookie()); res.redirect(next); }); adminRouter.post("/logout", (_req, res) => { res.setHeader("Set-Cookie", clearSessionCookie()); res.redirect("/"); }); adminRouter.get("/admin", requireOwnerWeb, async (req, res) => { const repos = await listRepos(); const message = typeof req.query["ok"] === "string" ? req.query["ok"] : null; const error = typeof req.query["error"] === "string" ? req.query["error"] : null; res.type("html").send( layout({ title: "Beheer", isOwner: true, body: adminPage(repos, message, error), }), ); }); adminRouter.post("/admin/repos", requireOwnerWeb, async (req, res) => { const name = typeof req.body?.name === "string" ? req.body.name.trim() : ""; const description = typeof req.body?.description === "string" ? req.body.description.trim() : ""; try { await createRepo(name, description); res.redirect(backToAdmin(`Repository "${name}" aangemaakt (privé).`, null)); } catch (error) { res.redirect(backToAdmin(null, (error as Error).message)); } }); adminRouter.post("/admin/repos/:name/visibility", requireOwnerWeb, async (req, res) => { const name = String(req.params["name"] ?? ""); const makePublic = req.body?.public === "1"; try { await updateRepo(name, { public: makePublic }); res.redirect( backToAdmin(`"${name}" staat nu op ${makePublic ? "publiek" : "privé"}.`, null), ); } catch (error) { res.redirect(backToAdmin(null, (error as Error).message)); } }); adminRouter.post("/admin/repos/:name/description", requireOwnerWeb, async (req, res) => { const name = String(req.params["name"] ?? ""); const description = typeof req.body?.description === "string" ? req.body.description.trim() : ""; try { await updateRepo(name, { description }); res.redirect(backToAdmin(`Beschrijving van "${name}" bijgewerkt.`, null)); } catch (error) { res.redirect(backToAdmin(null, (error as Error).message)); } }); adminRouter.post("/admin/repos/:name/delete", requireOwnerWeb, async (req, res) => { const name = String(req.params["name"] ?? ""); const confirm = typeof req.body?.confirm === "string" ? req.body.confirm.trim() : ""; if (confirm !== name) { res.redirect(backToAdmin(null, "Typ de naam exact over om te verwijderen.")); return; } try { await deleteRepo(name); res.redirect(backToAdmin(`"${name}" verwijderd.`, null)); } catch (error) { res.redirect(backToAdmin(null, (error as Error).message)); } });