Say tenants are Enterprise only where the installation asks, as the demo will

On a server that is not Enterprise the Tenants page is still only the
notice. On Enterprise the notice is gone -- a real installation that has
tenants has the licence -- unless SHOW_ENTERPRISE_NOTICES=1 asks for it above
the list. The public demo will set it: it reports Enterprise so tenants can be
shown, and should not suggest they come without the licence. The setting
reaches the browser as session.ihasmail.server.enterpriseNotices.
This commit is contained in:
2026-09-15 09:43:04 -07:00
parent 40df0f658b
commit 5f70e5e8d1
6 changed files with 69 additions and 17 deletions
+2
View File
@@ -40,6 +40,8 @@ export interface JmapSession {
edition?: string | null;
/** Where Stalwart's own administration is (STALWART_ADMIN_URL), for a session that may administer. */
adminUrl?: string | null;
/** SHOW_ENTERPRISE_NOTICES: an Enterprise-only section says so even on Enterprise. */
enterpriseNotices?: boolean;
};
/**
* False when this session may not administer: the operator turned it off,
+18 -8
View File
@@ -18,14 +18,17 @@ const PAGE_SIZE = 50;
* Tenants: separate organisations on one server, each with its own people,
* domains and limits.
*
* The section is offered to whoever may read tenants, but on a server that does
* not report Enterprise the page is only the notice: tenants there hold nobody
* to anything beyond an ordinary user's permissions, so there is nothing worth
* creating or listing. A server that reports no edition at all counts as not
* Enterprise.
* The section is offered to whoever may read tenants. On a server that does not
* report Enterprise -- or reports no edition -- the page is only a notice that
* tenants are an Enterprise feature: tenants there hold nobody to anything
* beyond an ordinary user's permissions, so there is nothing worth creating or
* listing. On Enterprise the notice is left out, unless the installation asks
* for it (SHOW_ENTERPRISE_NOTICES), as the public demo does so as not to
* suggest tenants come without the licence.
*/
export function TenantsAdmin({ selectedId }: { selectedId?: string }) {
const edition = useSession((s) => s.session?.ihasmail?.server?.edition ?? null);
const notices = useSession((s) => s.session?.ihasmail?.server?.enterpriseNotices === true);
if (edition !== "enterprise") {
return (
<div>
@@ -35,14 +38,19 @@ export function TenantsAdmin({ selectedId }: { selectedId?: string }) {
<p className="lead">{t("Separate organisations on one server, each with its own people, domains and limits.")}</p>
</div>
</div>
<p className="admin-notice warn">{t("Tenants are a Stalwart Enterprise feature.")}</p>
<EnterpriseNotice warn />
</div>
);
}
return <EnterpriseTenants selectedId={selectedId} />;
return <EnterpriseTenants selectedId={selectedId} notice={notices} />;
}
function EnterpriseTenants({ selectedId }: { selectedId?: string }) {
/** Said on every Tenants page, Enterprise or not. */
function EnterpriseNotice({ warn }: { warn: boolean }) {
return <p className={`admin-notice${warn ? " warn" : ""}`}>{t("Tenants are a Stalwart Enterprise feature.")}</p>;
}
function EnterpriseTenants({ selectedId, notice }: { selectedId?: string; notice: boolean }) {
const [, navigate] = useLocation();
const perms = usePermissions();
const [text, setText] = useState("");
@@ -119,6 +127,8 @@ function EnterpriseTenants({ selectedId }: { selectedId?: string }) {
)}
</div>
{notice && <EnterpriseNotice warn={false} />}
<div className="admin-toolbar">
<label className="admin-search">
<Search size={16} aria-hidden="true" />
@@ -18,8 +18,8 @@ vi.mock("@/lib/adminTenants", async (original) => ({
const { TenantsAdmin } = await import("../TenantsAdmin");
const PERMS = ["sysTenantGet", "sysTenantQuery", "sysTenantCreate"];
const signIn = (edition: string | null) =>
useSession.setState({ session: { capabilities: {}, accounts: {}, primaryAccounts: {}, username: "[email protected]", ihasmail: { permissions: PERMS, server: { edition } } } as unknown as JmapSession });
const signIn = (edition: string | null, enterpriseNotices = false) =>
useSession.setState({ session: { capabilities: {}, accounts: {}, primaryAccounts: {}, username: "[email protected]", ihasmail: { permissions: PERMS, server: { edition, enterpriseNotices } } } as unknown as JmapSession });
/** Tenants are managed on Enterprise only; anywhere else the page is the notice and nothing more. */
describe("the Tenants page", () => {
@@ -55,11 +55,37 @@ describe("the Tenants page", () => {
});
}
it("lists and offers tenants on Enterprise, without the notice", async () => {
it("lists and offers tenants on Enterprise, and does not say they are Enterprise", async () => {
signIn("enterprise");
await render();
expect(host.querySelector(".admin-notice.warn")).toBeNull();
expect(host.querySelector(".admin-notice")).toBeNull();
expect(host.textContent).toContain("New tenant");
expect(host.querySelector(".admin-table")?.textContent).toContain("Acme Corp");
});
});
describe("the Tenants page where the installation asks for Enterprise notices", () => {
let host: HTMLDivElement;
let root: Root;
beforeEach(() => {
host = document.createElement("div");
document.body.appendChild(host);
root = createRoot(host);
});
afterEach(async () => {
await act(async () => root.unmount());
host.remove();
});
it("says tenants are Enterprise above the list, as the demo does", async () => {
signIn("enterprise", true);
const { hook } = memoryLocation({ path: "/admin/tenants" });
await act(async () => {
root.render(<Router hook={hook}><TenantsAdmin /></Router>);
});
await act(async () => {});
expect(host.querySelector(".admin-notice")?.textContent).toBe("Tenants are a Stalwart Enterprise feature.");
expect(host.querySelector(".admin-notice.warn")).toBeNull();
expect(host.querySelector(".admin-table")?.textContent).toContain("Acme Corp");
});
});