import type { Context, Hono } from "hono"; import { html } from "../pages/html.ts"; import { plural } from "../pages/layout.ts"; import { isDomain, saveConfig } from "../server/config.ts"; import { endOtherSessions, liveSessions } from "../server/login.ts"; import { type AdminContext, adminPage, formField, notice, textFields } from "./console.ts"; export const MIN_PASSWORD = 12; /** * The Account section (June, 2026-09-30): the admin password, the sessions signed in, and the * domain, each written to kepr.toml by Kepr and applied without a restart. */ export function registerAccount(app: Hono, ctx: AdminContext): void { app.get("/admin/account", (c) => c.html(accountPage(c, ctx, ""))); app.post("/admin/account", async (c) => { const fields = await textFields(c); const refuse = (error: string) => c.html(accountPage(c, ctx, error), 400); const save = async (change: Partial) => { await saveConfig(ctx.configPath, { ...ctx.config, ...change }); Object.assign(ctx.config, change); }; if (fields.action === "password") { const password = fields.password ?? ""; const current = fields.current_password ?? ""; if (!(await Bun.password.verify(current, ctx.config.adminPasswordHash))) { return refuse("The current password is not right."); } if (password.length < MIN_PASSWORD) { return refuse(`The new password needs at least ${MIN_PASSWORD} characters.`); } if (password !== fields.confirm) return refuse("The two new passwords differ."); await save({ adminPasswordHash: await Bun.password.hash(password, "argon2id") }); // Whoever knew the old password is signed out with it. endOtherSessions(ctx, c); } else if (fields.action === "sessions") { endOtherSessions(ctx, c); } else if (fields.action === "domain") { const domain = (fields.domain ?? "").trim().toLowerCase(); if (!isDomain(domain)) { return refuse( "The domain is a bare hostname, such as code.example.com, with an optional :port.", ); } await save({ domain }); } else { return refuse("Unknown action."); } return c.redirect("/admin/account?saved=1", 303); }); } function accountPage(c: Context, ctx: AdminContext, error: string): string { const password = (name: string, label: string, autocomplete: string) => html` `; const body = html`${notice(error)}
${formField(c)} ${password("current_password", "current password", "current-password")} ${password("password", `new password (at least ${MIN_PASSWORD} characters)`, "new-password")} ${password("confirm", "new password again", "new-password")}

Changing it signs out every other session.

${plural(liveSessions(ctx), "session")} signed in, this one included. A session lasts 30 days.

${formField(c)}
${formField(c)}

Where Kepr is reached: pages, feeds and install.sh use it, and the console accepts forms only from it. Point the new name at this server first, then save and sign in there.

`; return adminPage(c, ctx, "account", body); }