account.ts

4.2 KB · raw

1import type { Context, Hono } from "hono";2import { html } from "../pages/html.ts";3import { plural } from "../pages/layout.ts";4import { isDomain, saveConfig } from "../server/config.ts";5import { endOtherSessions, liveSessions } from "../server/login.ts";6import { type AdminContext, adminPage, formField, notice, textFields } from "./console.ts";78export const MIN_PASSWORD = 12;910/**11 * The Account section (June, 2026-09-30): the admin password, the sessions signed in, and the12 * domain, each written to kepr.toml by Kepr and applied without a restart.13 */14export function registerAccount(app: Hono, ctx: AdminContext): void {15	app.get("/admin/account", (c) => c.html(accountPage(c, ctx, "")));1617	app.post("/admin/account", async (c) => {18		const fields = await textFields(c);19		const refuse = (error: string) => c.html(accountPage(c, ctx, error), 400);20		const save = async (change: Partial<typeof ctx.config>) => {21			await saveConfig(ctx.configPath, { ...ctx.config, ...change });22			Object.assign(ctx.config, change);23		};24		if (fields.action === "password") {25			const password = fields.password ?? "";26			const current = fields.current_password ?? "";27			if (!(await Bun.password.verify(current, ctx.config.adminPasswordHash))) {28				return refuse("The current password is not right.");29			}30			if (password.length < MIN_PASSWORD) {31				return refuse(`The new password needs at least ${MIN_PASSWORD} characters.`);32			}33			if (password !== fields.confirm) return refuse("The two new passwords differ.");34			await save({ adminPasswordHash: await Bun.password.hash(password, "argon2id") });35			// Whoever knew the old password is signed out with it.36			endOtherSessions(ctx, c);37		} else if (fields.action === "sessions") {38			endOtherSessions(ctx, c);39		} else if (fields.action === "domain") {40			const domain = (fields.domain ?? "").trim().toLowerCase();41			if (!isDomain(domain)) {42				return refuse(43					"The domain is a bare hostname, such as code.example.com, with an optional :port.",44				);45			}46			await save({ domain });47		} else {48			return refuse("Unknown action.");49		}50		return c.redirect("/admin/account?saved=1", 303);51	});52}5354function accountPage(c: Context, ctx: AdminContext, error: string): string {55	const password = (name: string, label: string, autocomplete: string) =>56		html`<label class="issues-label" for="account-${name}">${label}</label>57<input class="issues-input" id="account-${name}" name="${name}" type="password"58autocomplete="${autocomplete}" required>`;59	const body = html`${notice(error)}60<section class="repo-section"><h2 class="section-label mono">password</h2>61<form class="issues-form" method="post" action="/admin/account">62${formField(c)}<input type="hidden" name="action" value="password">63${password("current_password", "current password", "current-password")}64${password("password", `new password (at least ${MIN_PASSWORD} characters)`, "new-password")}65${password("confirm", "new password again", "new-password")}66<p class="issue-meta mono">Changing it signs out every other session.</p>67<button class="issues-submit" type="submit">change password</button>68</form></section>69<section class="repo-section"><h2 class="section-label mono">sessions</h2>70<p>${plural(liveSessions(ctx), "session")} signed in, this one included. A session lasts 30 days.</p>71<form method="post" action="/admin/account">72${formField(c)}<input type="hidden" name="action" value="sessions">73<button class="issues-submit" type="submit">sign out everywhere else</button>74</form></section>75<section class="repo-section"><h2 class="section-label mono">domain</h2>76<form class="issues-form" method="post" action="/admin/account">77${formField(c)}<input type="hidden" name="action" value="domain">78<label class="issues-label" for="account-domain">domain</label>79<input class="issues-input mono" id="account-domain" name="domain" value="${ctx.domain}"80autocomplete="off" spellcheck="false" required>81<p class="issue-meta mono">Where Kepr is reached: pages, feeds and install.sh use it, and the console82accepts forms only from it. Point the new name at this server first, then save and sign in there.</p>83<button class="issues-submit" type="submit">save domain</button>84</form></section>`;85	return adminPage(c, ctx, "account", body);86}