Menu
Public repository / 8b66e18ce8cb51ca3e4124f89fd1429a1d4792f3
AkurAI-Mail / src/framework/admin.js
Latest change 054e5cda0171bd13facff3c16a38f2048a1052cc - OLI-249: compose, settings, admin console and sign-in on the framework; SvelteKit removed by Ólafur Búi Ólafsson
// admin.js — the admin console pages on the framework runtime (plain ESM, no build step).
//
// Every admin HTML shell carries `data-admin-page`; this module signs the admin in (anyone else
// goes to the webmail or the IdP), loads the page's data from the API and renders it. Actions
// post to /api/actions and reload the page data on success, like the Svelte pages did.
import { effect, signal } from "./vendor/signals.js";
import { hydrate, registerIsland } from "./vendor/islands.js";
import { h, replace } from "./dom.js";
import { icon } from "./icons.js";
import { createApi, loginUrl } from "./api.js";
import { webmailSession } from "../lib/webmail/session.js";
export const NAV_ITEMS = [
{ href: "/", label: "Dashboard" },
{ href: "/mailboxes", label: "Mailboxes" },
{ href: "/domains", label: "Domains" },
{ href: "/antispam", label: "Anti-spam" },
{ href: "/dns", label: "DNS" },
{ href: "https://monitor.olibuijr.com/", label: "Monitoring", external: true },
{ href: "/webmail", label: "Webmail" },
];
/** @type {Record<string, string>} */
const SERVICE_LABELS = { postfix: "Postfix", dovecot: "Dovecot", opendkim: "OpenDKIM", nginx: "Nginx", "akurai-mail-ui": "Console", fail2ban: "Fail2ban" };
/**
* @param {string} href
* @param {string} pathname
*/
export function isActiveNav(href, pathname) {
const path = pathname.replace(/\/$/, "") || "/";
return href === "/" ? path === "/" : path.startsWith(href);
}
/** @param {any} check */
export const checkText = (check) => (check.values ?? []).join(", ") || check.detail || check.error || "no answer";
/** @param {any} value */
export function resultTitle(value) {
if (value?.verified) return "Domain verified";
if (value?.configured) return "Domain configured";
return "Domain needs attention";
}
/** @param {number | undefined} seconds */
const dateTime = (seconds) => (seconds ? new Date(seconds * 1000).toLocaleString() : "none");
/**
* @typedef {object} Ctx
* @property {ReturnType<typeof createApi>} api
* @property {() => Promise<void>} reload
* @property {(payload: Record<string, unknown>) => Promise<any>} action posts to /api/actions, reloads on success
* @property {import("./vendor/signals.js").signal extends (v: any) => infer S ? S : never} busy
* @property {any} result last action result (signal)
*/
/**
* @param {string} kicker @param {string} title @param {any} subtitle @param {Ctx} ctx
*/
const topbar = (kicker, title, subtitle, ctx) => h("header", { class: "topbar" },
h("div", null, h("p", { class: "kicker" }, kicker), h("h1", null, title), h("p", null, subtitle)),
h("button", { class: "primary", disabled: ctx.busy(), onclick: () => ctx.reload() }, icon("RefreshCcw", { size: 17 }), h("span", null, "Refresh")));
/** @param {any} result @param {(r: any) => any} [success] */
function noticeFor(result, success) {
if (!result) return null;
if (result.ok) return success ? success(result) : h("section", { class: "notice success", "aria-live": "polite" }, h("strong", null, "Action complete"));
if (result.error) return h("section", { class: "notice error", role: "alert" }, h("strong", null, "Action failed"), h("span", null, result.error));
return null;
}
/** @param {any[]} checks @param {string} fixLabel */
const healthList = (checks, fixLabel) => h("div", { class: "health-list" }, (checks ?? []).map((check) =>
h("div", null, h("b", { class: check.ok ? "ok" : null }, check.ok ? "OK" : fixLabel), h("span", null, check.type ? `${check.type} ${check.name}` : check.name), h("code", null, checkText(check)))));
/** @param {any[]} records */
const recordList = (records) => h("div", { class: "records" }, (records ?? []).map((record) => h("div", null, h("b", null, record.type), h("span", null, record.name), h("code", null, record.value))));
/** @type {Record<string, { load: (ctx: Ctx) => Promise<any>, render: (data: any, ctx: Ctx) => any }>} */
export const PAGES = {
dashboard: {
load: (ctx) => ctx.api.api("/api/status"),
render: (status, ctx) => [
topbar("Operations console", "Mail platform control plane", `${status.publicIp} / ${status.domain}`, ctx),
h("section", { class: "service-strip", "aria-label": "Service health" }, Object.entries(status.services ?? {}).map(([name, value]) =>
h("article", { class: value === "active" ? "ok" : null }, h("span", null, SERVICE_LABELS[name] ?? name), h("strong", null, String(value))))),
h("section", { class: "split" },
h("article", { class: "panel" }, h("div", { class: "panel-head" }, h("div", null, h("h2", null, "Mail queue"), h("p", null, status.queue?.summary ?? "Queue status unavailable")), h("strong", { class: "big" }, String(status.queue?.count ?? 0)))),
h("article", { class: "panel" },
h("div", { class: "panel-head" }, h("div", null, h("h2", null, "Delivery errors"), h("p", null, "Recent Postfix warnings, rejects, and bounces"))),
h("div", { class: "log-list" }, status.deliveryErrors?.length ? status.deliveryErrors.map((/** @type {string} */ line) => h("code", null, line)) : h("p", { class: "empty" }, "No delivery errors in the recent journal sample.")))),
],
},
mailboxes: {
load: async (ctx) => { const status = await ctx.api.api("/api/status"); return { users: status.users ?? [], aliases: status.aliases ?? [] }; },
render: ({ users, aliases }, ctx) => {
const result = ctx.result();
const userEmail = h("input", { type: "email", placeholder: "name@olibuijr.com", required: true });
const alias = h("input", { type: "email", placeholder: "alias@olibuijr.com", required: true });
const target = h("input", { type: "email", placeholder: "target@olibuijr.com", required: true });
return [
topbar("Mailbox management", "Mailboxes & Aliases", `${users.length} mailboxes, ${aliases.length} aliases`, ctx),
result?.password ? h("section", { class: "notice success", "aria-live": "polite" }, h("strong", null, "Mail-client password (IMAP/SMTP)"), h("code", null, result.password)) : noticeFor(result),
h("section", { class: "split" },
h("article", { class: "panel" },
h("div", { class: "panel-head" }, h("div", null, h("h2", null, "Mailboxes"), h("p", null, `${users.length} active`))),
h("form", { onsubmit: (/** @type {Event} */ e) => { e.preventDefault(); ctx.action({ action: "add-user", email: /** @type {HTMLInputElement} */ (userEmail).value }); } },
h("label", null, h("span", null, "Email"), userEmail), h("button", { class: "primary", disabled: ctx.busy() }, "Add")),
h("div", { class: "table" }, users.length ? users.map((/** @type {any} */ user) => h("div", null, h("span", null, h("strong", null, user.email), h("small", null, `${user.quotaUsedMb ?? 0} MB used · last delivery ${dateTime(user.lastDelivery)}`)))) : h("p", { class: "empty" }, "No mailboxes are configured."))),
h("article", { class: "panel" },
h("div", { class: "panel-head" }, h("div", null, h("h2", null, "Aliases"), h("p", null, `${aliases.length} active`))),
h("form", { class: "alias-form", onsubmit: (/** @type {Event} */ e) => { e.preventDefault(); ctx.action({ action: "add-alias", alias: /** @type {HTMLInputElement} */ (alias).value, target: /** @type {HTMLInputElement} */ (target).value }); } },
h("label", null, h("span", null, "Alias"), alias), h("label", null, h("span", null, "Target"), target), h("button", { class: "primary", disabled: ctx.busy() }, "Add")),
h("div", { class: "table" }, aliases.length ? aliases.map((/** @type {any} */ item) => h("div", null, h("span", null, item.alias), h("em", null, item.target))) : h("p", { class: "empty" }, "No aliases are configured.")))),
];
},
},
domains: {
load: async (ctx) => { const [domainData, status] = await Promise.all([ctx.api.api("/api/domain-list"), ctx.api.api("/api/status")]); return { domains: domainData.domains ?? [], status }; },
render: ({ domains, status }, ctx) => {
const result = ctx.result();
const checking = signal(/** @type {string | null} */ (null));
const configuring = signal(/** @type {string | null} */ (null));
const checkResult = signal(/** @type {any} */ (null));
const newDomain = /** @type {HTMLInputElement} */ (h("input", { type: "text", placeholder: "example.com", required: true, pattern: "^[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)+$" }));
/** @param {string} domain */
const checkDns = async (domain) => { checking(domain); checkResult(null); try { checkResult(await ctx.api.post("/api/actions", { action: "domain-check", domain })); } finally { checking(null); } };
/** @param {string} domain */
const autopilot = async (domain) => { configuring(domain); try { await ctx.action({ action: "domain-autopilot", domain }); } finally { configuring(null); } };
const checkPanel = h("div");
effect(() => {
const value = checkResult();
replace(checkPanel, value ? h("section", { class: "panel check-result" },
h("div", { class: "panel-head" }, h("div", null, h("h2", null, `DNS check: ${value.domain}`), h("p", null, "Status: ", h("strong", { class: value.status }, value.status))), h("button", { class: "secondary", onclick: () => checkResult(null) }, icon("X", { size: 16 }), h("span", null, "Dismiss"))),
h("h3", null, "Server configuration"), healthList(value.localChecks, "Fix"),
h("h3", null, "DNS verification"), healthList(value.dnsChecks, "Missing")) : null);
});
const list = h("div", { style: { display: "contents" } });
effect(() => {
const busyCheck = checking();
const busyConfig = configuring();
replace(list, domains.length ? domains.map((/** @type {any} */ d) => h("section", { class: "panel domain-panel" },
h("div", { class: "panel-head" },
h("div", null, h("h2", null, d.domain), h("p", null, h("span", { class: `status-badge ${d.status ?? ""}` }, d.status ?? ""), " ", d.addedAt ? `Added ${new Date(d.addedAt * 1000).toLocaleDateString()}` : "Primary domain")),
h("div", { class: "domain-actions" },
h("button", { class: "primary compact", disabled: busyConfig === d.domain, onclick: () => autopilot(d.domain) }, icon("Bot", { size: 16 }), " ", busyConfig === d.domain ? "Configuring..." : "Configure & verify"),
h("button", { class: "secondary", disabled: busyCheck === d.domain, onclick: () => checkDns(d.domain) }, icon("SearchCheck", { size: 16 }), " ", busyCheck === d.domain ? "Checking..." : "Verify DNS"),
d.domain !== status.domain ? h("button", { class: "danger", disabled: ctx.busy(), onclick: () => ctx.action({ action: "domain-remove", domain: d.domain }) }, icon("Trash2", { size: 16 }), h("span", null, "Remove")) : null)),
h("div", { class: "dns-section" }, h("h3", null, icon("ServerCog", { size: 16 }), " Server configuration"), healthList(d.localChecks, "Fix")),
h("div", { class: "dns-section" }, h("h3", null, "Required DNS records"), h("p", { class: "dns-hint" }, "Add these records at your domain registrar's DNS settings. All records are for the domain ", h("strong", null, d.domain), "."), recordList(d.dnsRecords)),
)) : h("section", { class: "panel" }, h("p", { class: "empty" }, "No domains configured. Add your first domain above.")));
});
return [
topbar("Domain management", "Mail domains", `Configure domains hosted on ${status.hostname}`, ctx),
result?.ok && result.dnsRecords ? h("section", { class: "notice success", "aria-live": "polite" }, h("strong", null, resultTitle(result)), h("span", null, `${result.domain}: ${result.status}. Publish any missing DNS records shown below.`))
: result?.ok && result.removed ? h("section", { class: "notice success", "aria-live": "polite" }, h("strong", null, "Domain removed"), h("span", null, `${result.domain} has been removed`))
: result?.error ? noticeFor(result) : null,
checkPanel,
h("section", { class: "panel" },
h("div", { class: "panel-head" }, h("div", null, h("h2", null, "Add domain"), h("p", null, "Register a domain, repair local mail-server config, generate DKIM, and verify public DNS."))),
h("form", { onsubmit: (/** @type {Event} */ e) => { e.preventDefault(); ctx.action({ action: "domain-autopilot", domain: newDomain.value }); newDomain.value = ""; } },
h("label", null, h("span", null, "Domain name"), newDomain), h("button", { class: "primary", disabled: ctx.busy() }, icon("Plus", { size: 17 }), h("span", null, "Configure domain")))),
list,
];
},
},
antispam: {
load: async (ctx) => { const status = await ctx.api.api("/api/status"); return { antiSpam: status.antiSpam ?? {}, users: status.users ?? [] }; },
render: ({ antiSpam, users }, ctx) => {
const policy = antiSpam?.policy ?? {};
const result = ctx.result();
const field = /** @type {HTMLSelectElement} */ (h("select", null, [["blocklist", "Blocked senders"], ["allowlist", "Allowed senders/domains"], ["domainBlocklist", "Blocked domains"], ["subjectKeywords", "Spam keywords"], ["attachmentExtensions", "Blocked extensions"]].map(([value, label]) => h("option", { value }, label))));
const mode = /** @type {HTMLSelectElement} */ (h("select", null, [["add", "Add"], ["remove", "Remove"], ["set", "Replace"]].map(([value, label]) => h("option", { value }, label))));
const value = /** @type {HTMLInputElement} */ (h("input", { placeholder: "sender@example.com, example.net, keyword", required: true }));
const mailbox = /** @type {HTMLSelectElement} */ (h("select", null, h("option", { value: "" }, "All mailboxes"), users.map((/** @type {any} */ user) => h("option", { value: user.email }, user.email))));
/** @param {Record<string, unknown>} patch */
const configure = (patch) => ctx.action({ action: "anti-spam-config", policy: patch });
const threshold = Number(policy.threshold ?? 5);
/** @param {string} name @param {string} title */
const pillList = (name, title) => h("div", { class: "pill-list" }, h("h3", null, title), (policy[name] ?? []).length ? (policy[name] ?? []).map((/** @type {string} */ item) => h("code", null, item)) : h("p", { class: "empty" }, `No ${title.toLowerCase()}.`));
return [
topbar("Spam protection", "Anti-spam", `${antiSpam?.totalSpam ?? 0} quarantined · ${policy.stats?.moved ?? 0} moved by policy`, ctx),
noticeFor(result, (r) => h("section", { class: "notice success", "aria-live": "polite" }, h("strong", null, "Action complete"), h("span", null, `${r.moved ?? 0} message(s) quarantined`))),
h("section", { class: "panel" },
h("div", { class: "panel-head" }, h("div", null, h("h2", null, "Controls")), h("button", { class: "primary", disabled: ctx.busy(), onclick: () => ctx.action({ action: "anti-spam-scan", email: mailbox.value }) }, "Scan")),
h("div", { class: "spam-grid" },
h("article", null, h("span", null, "Status"), h("strong", null, policy.enabled ? "Enabled" : "Disabled"), h("button", { class: "secondary", disabled: ctx.busy(), onclick: () => configure({ enabled: !policy.enabled }) }, policy.enabled ? "Disable" : "Enable")),
h("article", null, h("span", null, "Quarantine"), h("strong", null, policy.autoMove ? "Auto move" : "Score only"), h("button", { class: "secondary", disabled: ctx.busy(), onclick: () => configure({ autoMove: !policy.autoMove }) }, policy.autoMove ? "Score only" : "Auto move")),
h("article", null, h("span", null, "Threshold"), h("strong", null, String(threshold)), h("div", { class: "stepper" }, h("button", { class: "secondary", disabled: ctx.busy(), onclick: () => configure({ threshold: Math.max(1, threshold - 1) }) }, "-"), h("button", { class: "secondary", disabled: ctx.busy(), onclick: () => configure({ threshold: Math.min(20, threshold + 1) }) }, "+"))),
h("article", null, h("span", null, "Last scan"), h("strong", null, policy.stats?.lastScan ? dateTime(policy.stats.lastScan) : "Never"), h("small", null, `${policy.stats?.scanned ?? 0} scanned`))),
h("form", { class: "spam-form", onsubmit: (/** @type {Event} */ e) => { e.preventDefault(); configure({ field: field.value, mode: mode.value, value: value.value }); value.value = ""; } },
h("label", null, h("span", null, "List"), field), h("label", null, h("span", null, "Mode"), mode), h("label", null, h("span", null, "Value"), value), h("button", { class: "primary", disabled: ctx.busy() }, "Apply")),
h("form", { class: "scan-form", onsubmit: (/** @type {Event} */ e) => { e.preventDefault(); ctx.action({ action: "anti-spam-scan", email: mailbox.value }); } },
h("label", null, h("span", null, "Scan mailbox"), mailbox), h("button", { class: "primary", disabled: ctx.busy() }, "Run scan")),
h("div", { class: "split compact" }, pillList("blocklist", "Blocked senders"), pillList("domainBlocklist", "Blocked domains")),
h("div", { class: "table spam-counts" }, (antiSpam?.mailboxes ?? []).map((/** @type {any} */ item) => h("div", null, h("span", null, item.email), h("em", null, `${item.spam} spam`))))),
];
},
},
dns: {
load: async (ctx) => { const [status, dns] = await Promise.all([ctx.api.api("/api/status"), ctx.api.api("/api/dns")]); return { dnsHealth: status.dnsHealth ?? [], records: dns.records ?? [] }; },
render: ({ dnsHealth, records }, ctx) => [
topbar("DNS configuration", "DNS Records", "1984 Hosting values expected by this VM", ctx),
h("section", { class: "panel" }, h("div", { class: "panel-head" }, h("div", null, h("h2", null, "Expected records"), h("p", null, `${records.length} records configured`))), recordList(records)),
h("section", { class: "panel" }, h("div", { class: "panel-head" }, h("div", null, h("h2", null, "Health checks"), h("p", null, "Live DNS resolution status"))),
dnsHealth.length ? healthList(dnsHealth, "Check") : h("p", { class: "empty" }, "No health check data available.")),
],
},
};
/** Signs the admin in and returns the API, the hostname and the page context. */
async function boot() {
const api = createApi();
const session = webmailSession(await api.api("/api/auth/check"));
if (!session.signedIn) { window.location.href = loginUrl(window.location.pathname); return new Promise(() => {}); }
if (!session.admin) { window.location.href = "/webmail"; return new Promise(() => {}); }
const status = await api.api("/api/status");
return { api, hostname: /** @type {string} */ (status.hostname) };
}
const ready = boot();
registerIsland("admin-rail", (/** @type {HTMLElement} */ el) => {
ready.then(({ hostname }) => {
replace(el, [
h("a", { class: "brand", href: "/", "aria-label": "AkurAI Mail admin home" }, h("span", { class: "mark" }, "A"), h("span", null, h("strong", null, "AkurAI Mail"), h("small", null, hostname))),
h("nav", { "aria-label": "Primary" }, NAV_ITEMS.map((item) => item.external
? h("a", { href: item.href, target: "_blank", rel: "noopener noreferrer" }, `${item.label} ↗`)
: h("a", { class: isActiveNav(item.href, window.location.pathname) ? "active" : null, href: item.href }, item.label))),
h("a", { class: "signout", href: "/auth/logout" }, "Sign out"),
]);
});
});
registerIsland("admin-page", (/** @type {HTMLElement} */ el) => {
ready.then(({ api }) => {
const name = el.closest("[data-admin-page]")?.getAttribute("data-admin-page") ?? "dashboard";
const page = PAGES[name];
if (!page) { replace(el, h("p", { class: "empty" }, `Unknown page ${name}`)); return; }
const data = signal(/** @type {any} */ (null));
const busy = signal(false);
const result = signal(/** @type {any} */ (null));
/** @type {Ctx} */
const ctx = {
api, busy, result,
reload: async () => { busy(true); try { data(await page.load(ctx)); } finally { busy(false); } },
action: async (payload) => {
busy(true); result(null);
try {
const outcome = await api.post("/api/actions", payload);
result(outcome);
if (outcome.ok) data(await page.load(ctx));
return outcome;
} finally { busy(false); }
},
};
effect(() => { const value = data(); busy(); result(); if (value) replace(el, page.render(value, ctx)); });
ctx.reload().catch((error) => replace(el, h("section", { class: "notice error", role: "alert" }, h("strong", null, "Could not load this page"), h("span", null, String(error?.message ?? error)))));
});
});
hydrate();