Each tenant's page carries its legacy protocols switch (LP-9 to LP-18)

A card above the tenant form: whether legacy mail protocols are on or off
for the organization, a one-click "Turn legacy protocols back on", and
"Turn off legacy protocols…", which opens -- before anything can change --
the impact panel with the tenant's own people (LP-15), the statement at
tenant scope and the typed confirmation (LP-16, LP-17). The statement reads
"everyone in {organization}" and names no ports and no firewall: a
tenant's switch closes nothing, other tenants share the ports (LP-13).

It reads and turns inbuxa:TenantProtocolPolicy, with the domain
permissions the server checks for it. It shows on the read-only tenant page
too: a tenant administrator reads its tenant without changing it (MT-12)
and may still turn the switch. Turning it back on while the server has
legacy protocols off is refused by the server, and the refusal is shown.

The banner (LP-18) falls back to the tenant's switch where the server's
can't be read -- inside a tenant -- and reads "off for your
organization".

The impact panel, statement and confirmation move to parts.tsx, shared by
Hardening and the tenant card; the statement takes its scope.

Checked by hand against a local server with a tenant, two of its users and
the admin signed in over IMAP: the tenant card's panel lists the tenant's
two users and not the admin; the statement reads "everyone in Example Co"
with no ports or firewall; turning it off makes the tenant's user get "NO
[ALERT] Your organization allows only INBUXA webmail and JMAP apps" over
IMAP; one click turns it back on; Hardening's panel lists all three. Not
checked by hand: the banner as a tenant administrator sees it.
This commit is contained in:
2026-09-21 13:39:56 -07:00
parent 2aff8c8a10
commit 11d85cd759
7 changed files with 484 additions and 172 deletions
@@ -15,24 +15,43 @@ import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ShieldCheck } from 'lucide-react';
import { useAccountStore } from '@/stores/accountStore';
import { fetchProtocolPolicy } from './protocolPolicy';
import { fetchProtocolPolicy, fetchTenantPolicy } from './protocolPolicy';
export const LEGACY_PROTOCOLS_VIEW = 'CustomComponent/LegacyProtocols';
export function LegacyProtocolsBanner() {
const { t } = useTranslation();
const canGet = useAccountStore((s) => s.hasObjectPermission('sysNetworkListener', 'Get'));
const [off, setOff] = useState(false);
const canGetServer = useAccountStore((s) => s.hasObjectPermission('sysNetworkListener', 'Get'));
const canGetTenant = useAccountStore((s) => s.hasObjectPermission('sysDomain', 'Get'));
const [off, setOff] = useState<null | 'server' | 'tenant'>(null);
useEffect(() => {
if (!canGet) return;
if (!canGetServer && !canGetTenant) return;
const controller = new AbortController();
fetchProtocolPolicy(controller.signal)
.then((policy) => setOff(policy.legacyProtocols === 'disabled'))
// A banner is not worth an error: an older server simply has no switch.
.catch(() => setOff(false));
const signal = controller.signal;
(async () => {
// The server's switch first. Inside a tenant it can't be read, and the
// tenant's own is the one to report (LP-18 at tenant scope).
try {
if (canGetServer) {
const policy = await fetchProtocolPolicy(signal);
if (!signal.aborted) setOff(policy.legacyProtocols === 'disabled' ? 'server' : null);
return;
}
} catch {
// Fall through to the tenant's.
}
try {
if (canGetTenant) {
const policy = await fetchTenantPolicy(null, signal);
if (!signal.aborted) setOff(policy.legacyProtocols === 'disabled' ? 'tenant' : null);
}
} catch {
// A banner is not worth an error: an older server simply has no switch.
}
})();
return () => controller.abort();
}, [canGet]);
}, [canGetServer, canGetTenant]);
if (!off) return null;
@@ -42,11 +61,18 @@ export function LegacyProtocolsBanner() {
<span>
{t('legacyProtocols.bannerLead', 'Legacy mail protocols are')}{' '}
<strong>{t('legacyProtocols.bannerOff', 'off')}</strong>{' '}
{t('legacyProtocols.bannerTail', 'on this server. Only INBUXA webmail and JMAP apps can sign in.')}
{off === 'server'
? t('legacyProtocols.bannerTail', 'on this server. Only INBUXA webmail and JMAP apps can sign in.')
: t(
'legacyProtocols.bannerTailTenant',
'for your organization. Only INBUXA webmail and JMAP apps can sign in.',
)}
</span>
<Link to={`/Settings/${LEGACY_PROTOCOLS_VIEW}`} className="font-medium text-primary hover:underline">
{t('legacyProtocols.review', 'Review')}
</Link>
{off === 'server' && (
<Link to={`/Settings/${LEGACY_PROTOCOLS_VIEW}`} className="font-medium text-primary hover:underline">
{t('legacyProtocols.review', 'Review')}
</Link>
)}
</div>
);
}