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:
+6
-4
@@ -1265,10 +1265,11 @@ Stalwart shows the server's English until it is translated.
|
||||
A tenant is a separate organisation on the same server — its own people,
|
||||
domains and limits, and an administrator who manages only what is in it. It is
|
||||
a Stalwart Enterprise feature. On a server that does not report Enterprise — or
|
||||
reports no edition at all — the page is only a notice that anyone inside a
|
||||
tenant has only an ordinary user's permissions: no list, no search, nothing to
|
||||
create. On Enterprise, for a role with `sysTenantQuery` and `sysTenantGet`,
|
||||
under Access:
|
||||
reports no edition at all — the page is only the notice *Tenants are a Stalwart
|
||||
Enterprise feature.*: no list, no search, nothing to create. On Enterprise the
|
||||
notice is left out, unless `SHOW_ENTERPRISE_NOTICES=1` asks for it above the
|
||||
list, as the public demo does. On Enterprise, for a role with `sysTenantQuery`
|
||||
and `sysTenantGet`, under Access:
|
||||
|
||||
- **List and search** tenants, with each one's storage and account limit.
|
||||
- **Create and edit** a tenant's name, logo (an https address, drawn through the
|
||||
@@ -1713,6 +1714,7 @@ wizard, because either would be state.
|
||||
| Variable | Default | Does |
|
||||
| --- | --- | --- |
|
||||
| `STALWART_URL` | — | Where Stalwart is; the JMAP session is discovered at `/.well-known/jmap` |
|
||||
| `SHOW_ENTERPRISE_NOTICES` | `0` | Say an Enterprise-only section (Tenants) is Enterprise-only even when the server is Enterprise. For a demo that reports Enterprise to show those sections; a real installation leaves it off |
|
||||
| `STALWART_ADMIN_URL` | — | Where a browser opens Stalwart's own administration, linked from the Administration dashboard. Separate from `STALWART_URL`, which is often an address only this server can reach; unset, the dashboard names Stalwart's administration without a link |
|
||||
| `APP_SECRET` | — | Key material for sealing sessions. **Required in production** — the server refuses to start without it |
|
||||
| `HOST` / `PORT` | `0.0.0.0` / `8080` | Listen address |
|
||||
|
||||
+6
-1
@@ -896,7 +896,12 @@ function sessionExtras(session: LiveSession, info: AccountInfo = { locale: null,
|
||||
* session that may administer -- where the operator says its own
|
||||
* administration is.
|
||||
*/
|
||||
server: { edition: info.edition, adminUrl: administrationAllowed(config.administration, session.remember) ? adminUrlFor(session.username) : null },
|
||||
server: {
|
||||
edition: info.edition,
|
||||
adminUrl: administrationAllowed(config.administration, session.remember) ? adminUrlFor(session.username) : null,
|
||||
/** SHOW_ENTERPRISE_NOTICES: say "Enterprise feature" on Enterprise too, as the demo does. */
|
||||
enterpriseNotices: config.showEnterpriseNotices,
|
||||
},
|
||||
/**
|
||||
* Whether this session may administer: the installation offers it
|
||||
* (ADMINISTRATION) and the person signed in on a device marked as their own.
|
||||
|
||||
@@ -307,6 +307,13 @@ export const config = {
|
||||
*/
|
||||
stalwartAdminUrl: process.env.STALWART_ADMIN_URL ? httpUrl(process.env.STALWART_ADMIN_URL, "STALWART_ADMIN_URL") : "",
|
||||
stalwartAdminUrls: stalwartServers.adminUrls,
|
||||
/**
|
||||
* Say that an Enterprise-only section is Enterprise-only even on an
|
||||
* Enterprise server. Off, as a real installation wants it; the public demo
|
||||
* turns it on, because it reports Enterprise to show those sections and
|
||||
* should not suggest they come without the licence.
|
||||
*/
|
||||
showEnterpriseNotices: bool("SHOW_ENTERPRISE_NOTICES", false),
|
||||
appSecret,
|
||||
trustProxy: bool("TRUST_PROXY", true),
|
||||
/**
|
||||
|
||||
@@ -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,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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user