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).
161 lines
5.4 KiB
TypeScript
161 lines
5.4 KiB
TypeScript
/*
|
||
* 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} />;
|
||
}
|