Files
inbuxa-admin/src/components/layout/MainContent.tsx
T
jcoffey-dev b6b54d2ac0 Settings › Security › Hardening: the legacy mail protocols switch
The screen for inbuxa:ProtocolPolicy, the server-wide switch that closes
IMAP, POP3 and ManageSieve (legacy-protocols spec). Reached as
CustomComponent/LegacyProtocols, which the server's schema places under
Settings › Security; a server without that link never shows it.

- The selector lists every mail protocol, with what the switch does to
  each and on which ports. SMTP and JMAP are shown locked, from the
  server's lockedProtocols rather than a list carried here, so unlocking
  later needs no admin release (LP-21).
- The statement is shown in full before the switch moves and while it is
  off, with the listeners that close by name and port, and the note that
  firewall rules and port-forwards are the operator's to reconcile
  (LP-16, LP-20).
- Turning it off takes the typed phrase "turn off legacy mail", matched
  exactly. Turning it back on is one click (LP-17).
- Listeners that could not be reopened stay listed, with a Try again
  (LP-5).
- A banner on the Security settings and the dashboard while it is off
  (LP-18). It stays silent on a server without the switch.

Visible to whoever may see listeners, changeable by whoever may update
them, matching the permissions the server checks.

Not here yet: the impact panel (LP-15), which needs the server to record
last use per protocol, and the tenant switch (LP-9 to LP-14).
2026-09-21 09:26:18 -07:00

161 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
import { lazy, Suspense, useEffect, type ComponentType, type ReactNode } from 'react';
import { useSchemaStore } from '@/stores/schemaStore';
import { useCacheStore } from '@/stores/cacheStore';
import { useAccountStore } from '@/stores/accountStore';
import { resolveObject } from '@/lib/schemaResolver';
import { DynamicList } from '@/components/lists/DynamicList';
import { DynamicForm } from '@/components/forms/DynamicForm';
import { DynamicViewPage } from '@/components/views/DynamicViewPage';
import { LoadingFallback } from '@/components/common/LoadingFallback';
import { LegacyProtocolsBanner } from '@/features/hardening/LegacyProtocolsBanner';
import type { Schema } from '@/types/schema';
function lazyFeature<M, P>(load: () => Promise<M>, select: (module: M) => ComponentType<P>) {
return lazy(() => load().then((module) => ({ default: select(module) })));
}
const DashboardView = lazyFeature(
() => import('@/features/dashboard/components/DashboardView'),
(m) => m.DashboardView,
);
const DeliveryTracePage = lazyFeature(
() => import('@/features/troubleshoot/DeliveryTracePage'),
(m) => m.DeliveryTracePage,
);
const LiveTracingPage = lazyFeature(
() => import('@/features/tracing/components/LiveTracingPage'),
(m) => m.LiveTracingPage,
);
const TraceDetailView = lazyFeature(
() => import('@/features/tracing/components/TraceDetailView'),
(m) => m.TraceDetailView,
);
const ConnectDnsPage = lazyFeature(
() => import('@/features/dns/ConnectDnsPage'),
(m) => m.ConnectDnsPage,
);
const ActionPage = lazyFeature(
() => import('@/features/actions/ActionPage'),
(m) => m.ActionPage,
);
const LegacyProtocolsPage = lazyFeature(
() => import('@/features/hardening/LegacyProtocolsPage'),
(m) => m.LegacyProtocolsPage,
);
interface MainContentProps {
viewName?: string;
id?: string;
section?: string;
}
export function MainContent({ viewName, id, section }: MainContentProps) {
const schema = useSchemaStore((s) => s.schema);
const invalidateAllObjectLists = useCacheStore((s) => s.invalidateAllObjectLists);
useEffect(() => {
invalidateAllObjectLists();
}, [viewName, invalidateAllObjectLists]);
return <Suspense fallback={<LoadingFallback />}>{renderView(schema, viewName, id, section)}</Suspense>;
}
function renderView(schema: Schema | null, viewName?: string, id?: string, section?: string): ReactNode {
if (!viewName) {
return <LoadingFallback />;
}
if (viewName.startsWith('Dashboard/')) {
const dashboardId = viewName.slice('Dashboard/'.length);
return <DashboardView dashboardId={dashboardId} section={section ?? ''} />;
}
// INBUXA: guided jobs. Always reached by choosing "Guide me", never by default.
if (viewName.startsWith('Wizard/')) {
const [, wizard, param] = viewName.split('/');
if (wizard === 'dns' && param) {
return <ConnectDnsPage domainId={param} />;
}
return (
<div className="rounded-lg border border-dashed p-12 text-center text-muted-foreground">
Unknown guide: {wizard}
</div>
);
}
if (viewName.startsWith('CustomComponent/')) {
const componentName = viewName.slice('CustomComponent/'.length);
if (componentName === 'Dashboard') {
const firstId = schema?.dashboards?.[0]?.id ?? 'overview';
return <DashboardView dashboardId={firstId} section={section ?? ''} />;
}
if (componentName === 'LiveDelivery') {
return <DeliveryTracePage />;
}
if (componentName === 'LiveTracing') {
return <LiveTracingPage />;
}
// INBUXA: Settings Security Hardening (legacy-protocols spec).
if (componentName === 'LegacyProtocols') {
return <LegacyProtocolsPage />;
}
return (
<div className="rounded-lg border border-dashed p-12 text-center text-muted-foreground">
Unknown component: {componentName}
</div>
);
}
if (!schema) {
return <div className="flex items-center justify-center p-8 text-muted-foreground">Loading...</div>;
}
const resolved = resolveObject(schema, viewName);
if (!resolved) {
return <div className="flex items-center justify-center p-8 text-destructive">Unknown view: {viewName}</div>;
}
if (resolved.objectName === 'x:Action') {
return <ActionPage viewName={viewName} />;
}
if (resolved.objectType.type === 'singleton') {
// INBUXA: the Security settings carry the legacy protocols banner (LP-18).
if (resolved.objectName === 'x:Security') {
return (
<div className="space-y-4">
<LegacyProtocolsBanner />
<DynamicForm viewName={viewName} objectId="singleton" />
</div>
);
}
return <DynamicForm viewName={viewName} objectId="singleton" />;
}
if (id === 'new') {
return <DynamicForm viewName={viewName} objectId={null} />;
}
if (id) {
if (resolved.objectName === 'x:Trace') {
return <TraceDetailView viewName={viewName} objectId={id} />;
}
const canUpdate = useAccountStore.getState().hasObjectPermission(resolved.permissionPrefix, 'Update');
if (!canUpdate) {
return <DynamicViewPage viewName={viewName} objectId={id} />;
}
return <DynamicForm viewName={viewName} objectId={id} />;
}
return <DynamicList viewName={viewName} />;
}