Add Administration, starting with accounts #334

Closed
opened 2026-09-13 22:11:53 +00:00 by jcoffey-dev · 0 comments
Owner

Summary

An account whose Stalwart role manages accounts now finds Administration in the account menu, top right. Nobody else sees the entry, and /admin sends them back to their mail.

Accounts

  • List and search: 50 per page, newest first (Stalwart's own order), showing role, storage and groups.
  • Create: display name, address, a generated password, role and storage limit.
  • Edit: display name, other addresses (aliases), role and storage limit. Saving sends only what changed.
  • Set a new password: written into the existing password credential.
  • Delete: after typing the address to confirm.

Each control appears only when the role has the matching permission (sysAccountCreate, sysAccountUpdate, sysAccountDestroy). Sign-in needs both sysAccountQuery and sysAccountGet.

Stateless. The server already called GET /api/account for the edition and threw the rest away. It now keeps the permissions list and sends it with the session. Every read and write is a JMAP x:Account, x:Domain or x:Role call through the existing /api/jmap proxy: no new route, store or volume. Stalwart decides every call and scopes a tenant admin to their own tenant; the client only hides what would fail. Permission names are read in both camelCase (what the source serialises) and kebab-case (what the docs show).

Guards Stalwart doesn't provide

  • Outranking accounts are read-only. Stalwart checks a role grant against the caller's permissions, but not a password-only change or a delete. An account with more permissions than the viewer is therefore shown read-only, and a role the viewer can't read counts as outranking.
  • Roles are offered only if the viewer holds all their permissions.
  • Your own account. You can't change your own role or delete the account you're signed in with. Your own password goes through Settings › Security, which re-seals the session; changing it here would sign you out.

Mock server. It gains a directory of about 30 accounts, groups, domains and roles, behind the same permission names. MOCK_ROLE sets who the demo user is: admin (default), tenant-admin, helpdesk or user.

Docs. New Administration section in FEATURES.md; a KNOWN-ISSUES.md entry listing what is read from source and not yet tested live; a ROADMAP.md entry for the sections that come next (groups, mailing lists, roles, domains with DNS records and DKIM keys, tenants).

Independent of #333, which fixes account requests going to the default server when a domain is mapped to its own Stalwart. Administration uses the same routing, so #333 should land first.

Not yet verified against a live Stalwart

Built against the 0.16.22 source and the mock. To check on a real server with an admin account:

  • the casing of permission names from /api/account;
  • a password written to credentials/<index>/secret is hashed and the credential keeps its id;
  • admin x: calls accept the Basic credential ihasmail proxies with;
  • Stalwart skips its grant check on a password-only change (the reason the read-only guard exists).

Testing

  • Driven in Chrome against npm run dev:mock, desktop and 420px wide:
    • menu gating; list, search and paging;
    • opening your own account (role and delete locked, password sent to Settings);
    • password reset, both refused (too short, with Stalwart's reason) and accepted;
    • adding an alias and changing the storage limit, confirmed with x:Account/get;
    • creating an account;
    • the delete dialog, which enables only once the address is typed.
  • Bug found and fixed while driving: the permissions set was rebuilt whenever the session object changed, so the lists behind the panel reloaded after every session refresh until the rate limiter stopped them. It is now keyed on the list's contents.
  • Unit tests:
    • permission gating, the outranking guard and role resolution;
    • password generation without modulo bias;
    • the password, alias and quota patches, and error messages;
    • the mock directory's refusals;
    • a component test for the panel's three guard cases.
  • Checks:
    • npm run typecheck: clean;
    • npm test -w server: 178/178;
    • npm test -w web: 1193/1193;
    • npm run i18n:check: clean;
    • npm run build: succeeds, with AdminView in its own lazy chunk.

Translations

68 new keys (67 strings and 1 plural, {n} accounts), added to all nine catalogues: de, es, fr, nl, pt-BR, ru, uk, ja, zh-Hans. Russian and Ukrainian carry one/few/many/other; Japanese and Chinese carry other only. Strings falling back to English: 16 before, 16 after in every language.

Merged 2026-09-13 as coffey-labs/ihasmail@16e0761ddf

Rebuilt from: git history, session transcript.

## Summary An account whose Stalwart role manages accounts now finds **Administration** in the account menu, top right. Nobody else sees the entry, and `/admin` sends them back to their mail. **Accounts** - **List and search:** 50 per page, newest first (Stalwart's own order), showing role, storage and groups. - **Create:** display name, address, a generated password, role and storage limit. - **Edit:** display name, other addresses (aliases), role and storage limit. Saving sends only what changed. - **Set a new password:** written into the existing password credential. - **Delete:** after typing the address to confirm. Each control appears only when the role has the matching permission (`sysAccountCreate`, `sysAccountUpdate`, `sysAccountDestroy`). Sign-in needs both `sysAccountQuery` and `sysAccountGet`. **Stateless.** The server already called `GET /api/account` for the edition and threw the rest away. It now keeps the `permissions` list and sends it with the session. Every read and write is a JMAP `x:Account`, `x:Domain` or `x:Role` call through the existing `/api/jmap` proxy: no new route, store or volume. Stalwart decides every call and scopes a tenant admin to their own tenant; the client only hides what would fail. Permission names are read in both camelCase (what the source serialises) and kebab-case (what the docs show). **Guards Stalwart doesn't provide** - **Outranking accounts are read-only.** Stalwart checks a role grant against the caller's permissions, but not a password-only change or a delete. An account with more permissions than the viewer is therefore shown read-only, and a role the viewer can't read counts as outranking. - **Roles are offered only if the viewer holds all their permissions.** - **Your own account.** You can't change your own role or delete the account you're signed in with. Your own password goes through Settings › Security, which re-seals the session; changing it here would sign you out. **Mock server.** It gains a directory of about 30 accounts, groups, domains and roles, behind the same permission names. `MOCK_ROLE` sets who the demo user is: `admin` (default), `tenant-admin`, `helpdesk` or `user`. **Docs.** New Administration section in FEATURES.md; a KNOWN-ISSUES.md entry listing what is read from source and not yet tested live; a ROADMAP.md entry for the sections that come next (groups, mailing lists, roles, domains with DNS records and DKIM keys, tenants). Independent of #333, which fixes account requests going to the default server when a domain is mapped to its own Stalwart. Administration uses the same routing, so #333 should land first. ## Not yet verified against a live Stalwart Built against the 0.16.22 source and the mock. To check on a real server with an admin account: - the casing of permission names from `/api/account`; - a password written to `credentials/<index>/secret` is hashed and the credential keeps its id; - admin `x:` calls accept the Basic credential ihasmail proxies with; - Stalwart skips its grant check on a password-only change (the reason the read-only guard exists). ## Testing - **Driven in Chrome** against `npm run dev:mock`, desktop and 420px wide: - menu gating; list, search and paging; - opening your own account (role and delete locked, password sent to Settings); - password reset, both refused (too short, with Stalwart's reason) and accepted; - adding an alias and changing the storage limit, confirmed with `x:Account/get`; - creating an account; - the delete dialog, which enables only once the address is typed. - **Bug found and fixed while driving:** the permissions set was rebuilt whenever the session object changed, so the lists behind the panel reloaded after every session refresh until the rate limiter stopped them. It is now keyed on the list's contents. - **Unit tests:** - permission gating, the outranking guard and role resolution; - password generation without modulo bias; - the password, alias and quota patches, and error messages; - the mock directory's refusals; - a component test for the panel's three guard cases. - **Checks:** - `npm run typecheck`: clean; - `npm test -w server`: 178/178; - `npm test -w web`: 1193/1193; - `npm run i18n:check`: clean; - `npm run build`: succeeds, with `AdminView` in its own lazy chunk. ## Translations **68 new keys** (67 strings and 1 plural, `{n} accounts`), added to all nine catalogues: de, es, fr, nl, pt-BR, ru, uk, ja, zh-Hans. Russian and Ukrainian carry one/few/many/other; Japanese and Chinese carry `other` only. Strings falling back to English: **16 before, 16 after** in every language. **Merged** 2026-09-13 as coffey-labs/ihasmail@16e0761ddff2 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.