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;91011121314export 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 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}