Add Administration, starting with accounts

An account whose Stalwart role manages accounts now finds Administration in
the account menu. It lists, searches, creates and edits accounts -- display
name, other addresses, role, storage limit -- sets a new password, and
deletes, each offered only when the role holds the matching permission.

The server keeps the permissions list from GET /api/account, which it
already called for the edition and threw the rest away. Everything else is
JMAP x:Account, x:Domain and x:Role calls through the existing /api/jmap
proxy, so nothing new is stored and Stalwart decides every call.

Stalwart checks a grant against the caller's permissions but not a password
change or a delete, so an account that outranks the viewer is shown
read-only. Your own password is changed in Settings, which re-seals the
session; changing it here would strand it.

The mock server gains a directory behind the same permission names, with
MOCK_ROLE choosing admin, tenant-admin, helpdesk or user.

68 new strings, translated in all nine catalogues; strings falling back to
English stay at 16.
This commit is contained in:
2026-09-13 15:11:55 -07:00
parent b0564679e6
commit 82e217155b
34 changed files with 2684 additions and 22 deletions
+3 -3
View File
@@ -33,8 +33,8 @@ test("an account with no locale set yields none, rather than a guess", () => {
});
test("neither answering leaves the locale unknown", () => {
assert.deepEqual(interpretAccountInfo([failed("s", "forbidden"), failed("a", "forbidden")]), { locale: null, edition: null });
assert.deepEqual(interpretAccountInfo([]), { locale: null, edition: null });
assert.deepEqual(interpretAccountInfo([failed("s", "forbidden"), failed("a", "forbidden")]), { locale: null, edition: null, permissions: [] });
assert.deepEqual(interpretAccountInfo([]), { locale: null, edition: null, permissions: [] });
});
test("locales that carry no language are dropped, not passed through", () => {
@@ -48,7 +48,7 @@ test("a server without the registry is not asked for anything", async () => {
// fails the whole request rather than the one call.
const session = { capabilities: { "urn:ietf:params:jmap:core": {}, "urn:ietf:params:jmap:mail": {} }, accounts: {}, primaryAccounts: {} };
const info = await getAccountInfo("session-unsupported", "Basic x", session as never);
assert.deepEqual(info, { locale: null, edition: null });
assert.deepEqual(info, { locale: null, edition: null, permissions: [] });
});
test("no capabilities at all is treated the same way", async () => {