A tenant is a separate organisation on one server: its own people, domains and limits, and an administrator who manages only what is in it. It gets a section under Access, gated by sysTenantQuery and sysTenantGet, with a notice on a server that does not report Enterprise, where anyone inside a tenant is held to an ordinary user's permissions. The panel edits the tenant's name, logo, role and limits. The logo is an https address, drawn through the image proxy the strict image policy requires, or an image data URL. Limits change one quotas/<name> pointer each, so the four ihasmail does not offer keep their values, and an empty field is no limit. The role is the most anyone inside can be allowed. Stalwart keeps no list on a tenant -- each account, group, domain, list and role names its own -- so what a tenant holds is counted with memberTenantId queries and shown against its limits. Domains are added and taken out from the tenant's panel, one memberTenantId change each; only a domain in no tenant can be added, and its accounts stay where they are. Delete is offered once every count reads zero. A tenant does nothing until someone administers it, so the account panel gains a Tenant choice for an administrator who can read tenants: an Administrator inside a tenant administers that tenant. Nobody moves their own account. The mock has a tenant holding a domain and an administrator, a spare domain to assign, memberTenantId filters on every query, and Stalwart's rule that only an administrator outside every tenant may move things into one. A test of taking a domain back out found that the mock's pointer handling dropped a top-level null instead of storing it, so nothing had ever been cleared that way; it stores null now, as the server reads it back. Nothing about tenants has been written on a live server: production has none. KNOWN-ISSUES says what was read from source. Thirty-nine new strings and one plural, in all nine catalogues.
44 lines
1.9 KiB
TypeScript
44 lines
1.9 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { Redirect } from "wouter";
|
|
import { adminSections, type AdminSection } from "@/lib/adminAccess";
|
|
import { AccountsAdmin } from "./AccountsAdmin";
|
|
import { AdminDashboard } from "./AdminDashboard";
|
|
import { DomainsAdmin } from "./DomainsAdmin";
|
|
import { GroupsAdmin } from "./GroupsAdmin";
|
|
import { ListsAdmin } from "./ListsAdmin";
|
|
import { RolesAdmin } from "./RolesAdmin";
|
|
import { TenantsAdmin } from "./TenantsAdmin";
|
|
import { currentAdminSection } from "./AdminNav";
|
|
import { usePermissions } from "./usePermissions";
|
|
|
|
const RENDER: Record<AdminSection, (id?: string) => ReactNode> = {
|
|
dashboard: () => <AdminDashboard />,
|
|
accounts: (id) => <AccountsAdmin selectedId={id} />,
|
|
groups: (id) => <GroupsAdmin selectedId={id} />,
|
|
lists: (id) => <ListsAdmin selectedId={id} />,
|
|
tenants: (id) => <TenantsAdmin selectedId={id} />,
|
|
roles: (id) => <RolesAdmin selectedId={id} />,
|
|
domains: (id) => <DomainsAdmin selectedId={id} />,
|
|
};
|
|
|
|
/**
|
|
* Administration: what the signed-in account's Stalwart role lets it manage.
|
|
*
|
|
* The page is only the open section. Its list of sections is in the folder
|
|
* pane (see AdminNav), so the tables here get the width Settings spends on a
|
|
* second column. A bare /admin opens the dashboard, and a section the role
|
|
* cannot read -- typed into the address bar, say -- opens the first one it can.
|
|
*/
|
|
export function AdminView({ section, id }: { section?: string; id?: string }) {
|
|
const allowed = adminSections(usePermissions());
|
|
// A role taken away since the menu was drawn. Stalwart would refuse every
|
|
// call anyway; this spares the page of refusals.
|
|
if (!allowed.length) return <Redirect to="/mail" />;
|
|
const current = currentAdminSection(allowed, section)!;
|
|
return (
|
|
<div className="admin-layout">
|
|
<div className="settings-content admin-content">{RENDER[current](section === current ? id : undefined)}</div>
|
|
</div>
|
|
);
|
|
}
|