AkurAI Build
Menu

Public repository / 054e5cda0171bd13facff3c16a38f2048a1052cc

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();