import { h, mount, field, input, toast, errorBox, time, modal, badge } from "../ui.js"; import { api, get } from "../api.js"; import * as totp from "../totp.js"; import { tokenTable, newTokenForm, grantHelp } from "./users.js"; export async function render(el) { const me = await get("/auth/whoami"); const grants = await get("/catalog/grants"); const tokensEl = h("div"); const drawTokens = async () => { const tokens = await get("/me/tokens"); mount(tokensEl, tokenTable(tokens, drawTokens), newTokenForm("/me/tokens", grants, drawTokens, { admin: me.admin, grants: me.user.grants })); }; const cur = input({ type: "password", autocomplete: "current-password" }); const pw1 = input({ type: "password", autocomplete: "new-password" }); const pw2 = input({ type: "password", autocomplete: "new-password" }); const err = h("div"); mount(el, h("h1", null, "Account: " + me.user.username), h("div", { class: "panel" }, h("h2", null, "Permissions"), h("div", { class: "grants" }, (me.permissions || []).map((p) => h("div", null, p)))), me.method === "session" ? twoFactorPanel(me) : null, h("div", { class: "panel" }, h("h2", null, "Change password"), h("div", { class: "form-grid" }, field("Current password", cur), field("New password", pw1, "At least 8 characters."), field("Repeat", pw2)), err, h("div", { class: "actions", style: { marginTop: "8px" } }, h("button", { class: "primary", onclick: async () => { mount(err); if (pw1.value !== pw2.value) { mount(err, h("div", { class: "msg err" }, "The new passwords don't match.")); return; } try { await api("POST", "/me/password", { body: { current_password: cur.value, password: pw1.value } }); toast("Password changed; other sessions were logged out"); cur.value = pw1.value = pw2.value = ""; } catch (e) { mount(err, errorBox(e)); } } }, "Change password"))), h("div", { class: "panel" }, h("h2", null, "My API tokens"), h("p", { class: "muted small" }, "Use a token with the jarl CLI (jarl config set token …) or any HTTP client: Authorization: Bearer ."), tokensEl), grantHelp(grants)); await drawTokens(); } // twoFactorPanel shows and changes the user's two-factor login. function twoFactorPanel(me) { const el = h("div", { class: "panel" }); const draw = async () => { const st = await get("/me/totp"); const body = h("div"); const redraw = () => draw(); if (!st.enabled) { mount(el, h("h2", null, "Two-factor login"), h("p", null, "Log in with your password and a code from an authenticator app on your phone, so a stolen password alone isn't enough.", st.required ? h("strong", null, " This server requires it for your account.") : null), body); totp.setup(body, me, redraw); return; } const actions = h("div", { class: "actions" }); mount(el, h("h2", null, "Two-factor login"), h("p", null, badge("on", "ok"), " since ", time(st.enabled_at), ". ", st.recovery_codes_left + " of 10 recovery codes left.", st.required ? h("span", { class: "muted" }, " Required on this server.") : null), st.recovery_codes_left <= 3 ? h("div", { class: "msg warn" }, "Only " + st.recovery_codes_left + " recovery codes are left: make new ones.") : null, body, actions); mount(actions, h("button", { onclick: () => { mount(actions); totp.setup(body, me, redraw, { again: true, cancel: redraw }); } }, "Move to a new phone"), h("button", { onclick: async () => { try { const r = await api("POST", "/me/totp/recovery-codes"); mount(actions); mount(body, totp.recoveryBox(r.recovery_codes, me.user.username), h("div", { class: "actions", style: { marginTop: "8px" } }, h("button", { class: "primary", onclick: redraw }, "I have saved the codes"))); } catch (e) { toast(e.message, "err", 8000); } } }, "New recovery codes"), st.required ? null : h("button", { class: "danger", onclick: () => disable(me, redraw) }, "Turn off")); }; draw(); return el; } async function disable(me, done) { const pw = input({ type: "password", autocomplete: "current-password" }); const code = input({ autocomplete: "one-time-code", placeholder: "123456 or a recovery code" }); const err = h("div"); const ok = await modal("Turn off two-factor login", h("div", null, h("p", null, "You will log in with your password alone."), h("div", { class: "form-grid" }, me.user.has_password ? field("Password", pw) : null, field("Code", code, "From your authenticator app, or a recovery code.")), err), [ { label: "Cancel", value: null }, { label: "Turn off", kind: "danger", onclick: async () => { try { await api("POST", "/me/totp/disable", { body: { password: pw.value, code: code.value } }); return true; } catch (e) { mount(err, errorBox(e)); return false; } } }, ]); if (ok) { toast("Two-factor login is off"); done(); } }