Find Stalwart's administration instead of asking for it
The dashboard's link to Stalwart's own administration needed
STALWART_ADMIN_URL, which an operator had to know to set. Everything it
holds can be read from the server:
- the origin is the host Stalwart advertises in its own session URLs, the
one people reach it at even when ihasmail talks to it on a private
address;
- the prefix is where its web interface application is installed. The
production server's x:Application reads "Stalwart Web Interface",
enabled, urlPrefix {"/admin", "/account"}, and /admin is also what
Stalwart writes at first boot.
So, for an account that administers, the account info fetch now asks the
account's own server for its applications and links to origin + /admin/.
An installation whose web interface is disabled or moved gets no link; an
administrator who may not read applications gets Stalwart's default
/admin. It is cached with the rest of the account info.
STALWART_ADMIN_URL and a servers file entry's adminUrl still win, for an
administration that lives somewhere else. A routed domain without one now
takes what its own server said, never the default server's.
This commit is contained in:
@@ -45,7 +45,7 @@ const OPS = ["Get", "Query", "Create", "Update", "Destroy"] as const;
|
||||
const all = (...objects: string[]) => objects.flatMap((o) => OPS.map((op) => `sys${o}${op}`));
|
||||
|
||||
/** What the dashboard reads beyond the directory. */
|
||||
const READ_SERVER = ["sysQueuedMessageGet", "sysQueuedMessageQuery", "sysMetricGet", "sysMetricQuery"];
|
||||
const READ_SERVER = ["sysQueuedMessageGet", "sysQueuedMessageQuery", "sysMetricGet", "sysMetricQuery", "sysApplicationGet", "sysApplicationQuery"];
|
||||
|
||||
/** A few of the ordinary ones, so the list looks like what a server sends. */
|
||||
const USER_PERMISSIONS = ["jmapEmailGet", "jmapEmailUpdate", "jmapMailboxGet", "sysAccountSettingsGet"];
|
||||
@@ -226,6 +226,8 @@ export function createDirectory(opts: Options) {
|
||||
push(4, "Counter", "queue.report-queued", h % 4 === 1 ? 2 : 0);
|
||||
}
|
||||
}
|
||||
const applications: Obj[] = [{ id: "app1", description: "Stalwart Web Interface", enabled: true, urlPrefix: { "/admin": true, "/account": true } }];
|
||||
|
||||
/** Tenants: a name, limits, and whatever names them in its memberTenantId. */
|
||||
const tenants: Obj[] = [
|
||||
{ id: "t1", name: "Acme Corp", logo: null, roles: { "@type": "Default" }, permissions: { "@type": "Inherit" }, quotas: { maxAccounts: 25, maxDomains: 2, maxDiskQuota: 50 * GIB }, createdAt: "2026-07-01T09:00:00Z" },
|
||||
@@ -693,6 +695,10 @@ export function createDirectory(opts: Options) {
|
||||
}
|
||||
return { accountId: opts.accountId, oldState: "1", newState: "2", created, updated, destroyed, ...(Object.keys(notCreated).length ? { notCreated } : {}), ...(Object.keys(notUpdated).length ? { notUpdated } : {}), ...(Object.keys(notDestroyed).length ? { notDestroyed } : {}) };
|
||||
},
|
||||
// Stalwart's web interface is an installed application; ihasmail reads its
|
||||
// prefix to link the dashboard to it.
|
||||
"x:Application/query": query(() => applications, "sysApplicationQuery", ["text"], () => true),
|
||||
"x:Application/get": get(applications, "sysApplicationGet"),
|
||||
"x:Role/get": get(roles, "sysRoleGet"),
|
||||
"x:Role/query": query(() => roles, "sysRoleQuery", ["text", "description", "memberTenantId"], (o, f) => (f.memberTenantId === undefined || (o.memberTenantId ?? null) === f.memberTenantId) && matchText(o, f.description)),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user