Minimale git-remote met publiek/prive-toggle

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

4.9 kB Ruw bestand
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));
  }
});