feat: add access module boundary migrations

This commit is contained in:
2026-07-10 12:51:16 +02:00
parent 37828fe340
commit 04681f1d75
41 changed files with 7229 additions and 738 deletions

View File

@@ -1,6 +1,13 @@
import { useEffect, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import type { AdminSectionContribution, AdminSectionsUiCapability, ApiSettings, AuthInfo, MailProfilesUiCapability } from "@govoplan/core-webui";
import type {
AdminSectionContribution,
AdminSectionsUiCapability,
ApiSettings,
AuthInfo,
FilesConnectorsUiCapability,
MailProfilesUiCapability
} from "@govoplan/core-webui";
import { fetchMe } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { ModuleSubnav, type ModuleSubnavGroup } from "@govoplan/core-webui";
@@ -14,6 +21,7 @@ import GroupsPanel from "./GroupsPanel";
import RolesPanel from "./RolesPanel";
import ApiKeysPanel from "./ApiKeysPanel";
import AdminAuditPanel from "./AdminAuditPanel";
import FileConnectorsPanel from "./FileConnectorsPanel";
import MailProfilesPanel from "./MailProfilesPanel";
import RetentionPoliciesPanel from "./RetentionPoliciesPanel";
import { usePlatformModuleInstalled, usePlatformUiCapabilities, usePlatformUiCapability } from "@govoplan/core-webui";
@@ -21,6 +29,38 @@ import { usePlatformModuleInstalled, usePlatformUiCapabilities, usePlatformUiCap
type AdminSection = string;
type OrderedAdminNavItem = { id: AdminSection; label: string; order: number };
const handledAdminSectionIds = new Set<string>([
"overview",
"system-settings",
"system-configuration-changes",
"system-configuration-packages",
"system-modules",
"system-audit",
"system-tenants",
"system-roles",
"system-role-templates",
"system-groups",
"system-users",
"system-file-connectors",
"system-mail-servers",
"system-retention",
"tenant-settings",
"tenant-roles",
"tenant-groups",
"tenant-users",
"tenant-file-connectors",
"tenant-mail-servers",
"tenant-api-keys",
"tenant-retention",
"tenant-audit",
"tenant-group-file-connectors",
"tenant-group-mail-servers",
"tenant-group-retention",
"tenant-user-file-connectors",
"tenant-user-mail-servers",
"tenant-user-retention"
]);
export default function AdminPage({
settings,
auth,
@@ -31,16 +71,19 @@ export default function AdminPage({
onAuthChange: (auth: AuthInfo | null, accessToken?: string) => void;
}) {
const mailProfilesUi = usePlatformUiCapability<MailProfilesUiCapability>("mail.profiles");
const fileConnectorsUi = usePlatformUiCapability<FilesConnectorsUiCapability>("files.connectors");
const adminSectionCapabilities = usePlatformUiCapabilities<AdminSectionsUiCapability>("admin.sections");
const mailProfilesAvailable = Boolean(mailProfilesUi);
const fileConnectorsAvailable = Boolean(fileConnectorsUi);
const auditAvailable = usePlatformModuleInstalled("audit");
const policyAvailable = usePlatformModuleInstalled("policy");
const tenancyAvailable = usePlatformModuleInstalled("tenancy");
const contributedSections = useMemo(() => (
adminSectionCapabilities
.flatMap((capability) => capability.sections)
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100))
), [adminSectionCapabilities]);
const contributedSections = useMemo(
() =>
adminSectionCapabilities
.flatMap((capability) => capability.sections)
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100)),
[adminSectionCapabilities]
);
const contributionById = useMemo(() => {
const mapped = new Map<string, AdminSectionContribution>();
for (const section of contributedSections) {
@@ -58,7 +101,7 @@ export default function AdminPage({
if (policyAvailable) sections.add("system-retention");
if (mailProfilesAvailable) sections.add("system-mail-servers");
}
if (tenancyAvailable && hasScope(auth, "system:tenants:read")) sections.add("system-tenants");
if (hasScope(auth, "system:tenants:read")) sections.add("system-tenants");
if (hasAnyScope(auth, ["system:accounts:read", "system:access:read"])) sections.add("system-users");
if (hasAnyScope(auth, ["system:roles:read", "system:access:read"])) sections.add("system-roles");
if (auditAvailable && hasScope(auth, "system:audit:read")) sections.add("system-audit");
@@ -71,6 +114,10 @@ export default function AdminPage({
if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-mail-servers");
if (hasScope(auth, "admin:groups:read")) sections.add("tenant-group-mail-servers");
}
if (fileConnectorsAvailable && hasAnyScope(auth, ["files:file:admin", "admin:settings:read"])) {
if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-file-connectors");
if (hasScope(auth, "admin:groups:read")) sections.add("tenant-group-file-connectors");
}
if (policyAvailable && hasScope(auth, "admin:policies:read")) {
sections.add("tenant-retention");
if (hasScope(auth, "admin:users:read")) sections.add("tenant-user-retention");
@@ -79,7 +126,7 @@ export default function AdminPage({
if (hasScope(auth, "admin:settings:read")) sections.add("tenant-settings");
if (auditAvailable && hasScope(auth, "audit:read")) sections.add("tenant-audit");
return sections;
}, [auth, auditAvailable, contributedSections, mailProfilesAvailable, policyAvailable, tenancyAvailable]);
}, [auth, auditAvailable, contributedSections, fileConnectorsAvailable, mailProfilesAvailable, policyAvailable]);
const [searchParams, setSearchParams] = useSearchParams();
const requestedSection = searchParams.get("section") as AdminSection | null;
const fallbackSection = available.has("overview") ? "overview" : (Array.from(available)[0] ?? "overview");
@@ -105,50 +152,85 @@ export default function AdminPage({
}, [settings.accessToken, settings.apiBaseUrl]);
if (!hasAnyScope(auth, adminReadScopes)) {
return <div className="content-pad"><Card title="Administration unavailable"><p>Your current roles do not grant administrative access.</p></Card></div>;
return (
<div className="content-pad">
<Card title="i18n:govoplan-access.administration_unavailable.b86d4cb5">
<p>i18n:govoplan-access.your_current_roles_do_not_grant_administrative_a.6eafee69</p>
</Card>
</div>
);
}
const rootItems = contributedNavItems(contributedSections, available, "ROOT");
const adminSubnav: ModuleSubnavGroup<AdminSection>[] = [
{ items: asSubnavItems(rootItems) },
{
title: "SYSTEM",
items: asSubnavItems(sortNavItems([
...contributedNavItems(contributedSections, available, "SYSTEM"),
visibleNavItem(available, "system-tenants", "Tenants", 20),
visibleNavItem(available, "system-roles", "System roles", 30),
visibleNavItem(available, "system-users", "Users", 60),
visibleNavItem(available, "system-mail-servers", "Mail servers", 70),
visibleNavItem(available, "system-retention", "Retention", 80),
visibleNavItem(available, "system-audit", "Audit", 90)
]))
title: "ADMINISTRATION",
items: asSubnavItems(
sortNavItems([
...contributedNavItems(contributedSections, available, "ROOT"),
visibleNavItem(available, "system-modules", "i18n:govoplan-access.modules.04e9462c", 10),
visibleNavItem(available, "system-configuration-packages", "i18n:govoplan-access.packages.0a999012", 20),
visibleNavItem(available, "system-settings", "i18n:govoplan-access.maintenance.94de303b", 30),
visibleNavItem(available, "system-configuration-changes", "i18n:govoplan-access.changes.8aa57de6", 40),
visibleNavItem(available, "system-audit", "i18n:govoplan-access.audit.fa1703dd", 90),
...contributedNavItems(contributedSections, available, "ADMINISTRATION", handledAdminSectionIds)
])
)
},
{
title: "GLOBAL",
items: asSubnavItems(
sortNavItems([
visibleNavItem(available, "system-tenants", "i18n:govoplan-access.tenants.1f7ae776", 10),
visibleNavItem(available, "system-roles", "i18n:govoplan-access.system_roles.a9461aa6", 20),
visibleNavItem(available, "system-role-templates", "i18n:govoplan-access.tenant_role_templates", 30),
visibleNavItem(available, "system-groups", "i18n:govoplan-access.group_templates", 40),
visibleNavItem(available, "system-users", "i18n:govoplan-access.users.57f2b181", 50),
visibleNavItem(available, "system-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 60),
visibleNavItem(available, "system-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 70),
visibleNavItem(available, "system-retention", "i18n:govoplan-access.retention.c7199d9e", 80),
...contributedNavItems(contributedSections, available, "GLOBAL", handledAdminSectionIds),
...contributedNavItems(contributedSections, available, "SYSTEM", handledAdminSectionIds)
])
)
},
{
title: "TENANT",
items: [
...(available.has("tenant-settings") ? [{ id: "tenant-settings" as const, label: "General" }] : []),
...(available.has("tenant-roles") ? [{ id: "tenant-roles" as const, label: "Roles" }] : []),
...(available.has("tenant-groups") ? [{ id: "tenant-groups" as const, label: "Groups" }] : []),
...(available.has("tenant-users") ? [{ id: "tenant-users" as const, label: "Users" }] : []),
...(available.has("tenant-mail-servers") ? [{ id: "tenant-mail-servers" as const, label: "Mail servers" }] : []),
...(available.has("tenant-retention") ? [{ id: "tenant-retention" as const, label: "Retention" }] : []),
...(available.has("tenant-api-keys") ? [{ id: "tenant-api-keys" as const, label: "API keys" }] : []),
...(available.has("tenant-audit") ? [{ id: "tenant-audit" as const, label: "Audit" }] : [])
]
items: asSubnavItems(
sortNavItems([
visibleNavItem(available, "tenant-roles", "i18n:govoplan-access.roles.47dcc27d", 10),
visibleNavItem(available, "tenant-groups", "i18n:govoplan-access.groups.ae9629f4", 20),
visibleNavItem(available, "tenant-users", "i18n:govoplan-access.users.57f2b181", 30),
visibleNavItem(available, "tenant-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 40),
visibleNavItem(available, "tenant-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 50),
visibleNavItem(available, "tenant-api-keys", "i18n:govoplan-access.api_keys.94fcf3c2", 60),
visibleNavItem(available, "tenant-retention", "i18n:govoplan-access.retention.c7199d9e", 70),
visibleNavItem(available, "tenant-settings", "i18n:govoplan-access.general.9239ee2c", 80),
visibleNavItem(available, "tenant-audit", "i18n:govoplan-access.audit.fa1703dd", 90),
...contributedNavItems(contributedSections, available, "TENANT", handledAdminSectionIds)
])
)
},
{
title: "GROUP",
items: [
...(available.has("tenant-group-mail-servers") ? [{ id: "tenant-group-mail-servers" as const, label: "Mail servers" }] : []),
...(available.has("tenant-group-retention") ? [{ id: "tenant-group-retention" as const, label: "Retention" }] : []),
]
items: asSubnavItems(
sortNavItems([
visibleNavItem(available, "tenant-group-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10),
visibleNavItem(available, "tenant-group-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20),
visibleNavItem(available, "tenant-group-retention", "i18n:govoplan-access.retention.c7199d9e", 30),
...contributedNavItems(contributedSections, available, "GROUP", handledAdminSectionIds)
])
)
},
{
title: "USER",
items: [
...(available.has("tenant-user-mail-servers") ? [{ id: "tenant-user-mail-servers" as const, label: "Mail servers" }] : []),
...(available.has("tenant-user-retention") ? [{ id: "tenant-user-retention" as const, label: "Retention" }] : []),
]
items: asSubnavItems(
sortNavItems([
visibleNavItem(available, "tenant-user-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10),
visibleNavItem(available, "tenant-user-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20),
visibleNavItem(available, "tenant-user-retention", "i18n:govoplan-access.retention.c7199d9e", 30),
...contributedNavItems(contributedSections, available, "USER", handledAdminSectionIds)
])
)
}
].filter((group) => group.items.length > 0);
const contributedSection = contributionById.get(active);
@@ -161,22 +243,24 @@ export default function AdminPage({
<div className="content-pad workspace-data-page">
{contributedSection && contributedSection.render(contributionContext)}
{!contributedSection && active === "system-retention" && <RetentionPoliciesPanel settings={settings} scopeType="system" canWrite={hasScope(auth, "system:settings:write")} />}
{!contributedSection && active === "system-mail-servers" && <MailProfilesPanel settings={settings} scopeType="system" canWriteProfiles={hasScope(auth, "system:settings:write")} canManageCredentials={hasScope(auth, "system:settings:write")} canWritePolicy={hasScope(auth, "system:settings:write")} />}
{!contributedSection && active === "system-tenants" && <TenantsPanel settings={settings} auth={auth} canCreate={hasScope(auth, "system:tenants:create")} canUpdate={hasScope(auth, "system:tenants:update")} canSuspend={hasScope(auth, "system:tenants:suspend")} onAuthRefresh={refreshAuth} />}
{!contributedSection && active === "system-users" && <SystemUsersPanel
settings={settings}
canCreate={hasScope(auth, "system:accounts:create")}
canUpdate={hasScope(auth, "system:accounts:update")}
canSuspend={hasScope(auth, "system:accounts:suspend")}
canAssignRoles={hasAnyScope(auth, ["system:roles:assign", "system:access:assign"])}
canManageMemberships={hasScope(auth, "system:accounts:update") && hasScope(auth, "system:access:assign")}
onAuthRefresh={refreshAuth}
/>}
{!contributedSection && active === "system-roles" && <SystemRolesPanel
settings={settings}
canWrite={hasScope(auth, "system:roles:write")}
onAuthRefresh={refreshAuth}
/>}
{!contributedSection && active === "system-mail-servers" && (
<MailProfilesPanel settings={settings} scopeType="system" canWriteProfiles={hasScope(auth, "system:settings:write")} canManageCredentials={hasScope(auth, "system:settings:write")} canWritePolicy={hasScope(auth, "system:settings:write")} />
)}
{!contributedSection && active === "system-tenants" && (
<TenantsPanel settings={settings} auth={auth} canCreate={hasScope(auth, "system:tenants:create")} canUpdate={hasScope(auth, "system:tenants:update")} canSuspend={hasScope(auth, "system:tenants:suspend")} onAuthRefresh={refreshAuth} />
)}
{!contributedSection && active === "system-users" && (
<SystemUsersPanel
settings={settings}
canCreate={hasScope(auth, "system:accounts:create")}
canUpdate={hasScope(auth, "system:accounts:update")}
canSuspend={hasScope(auth, "system:accounts:suspend")}
canAssignRoles={hasAnyScope(auth, ["system:roles:assign", "system:access:assign"])}
canManageMemberships={hasScope(auth, "system:accounts:update") && hasScope(auth, "system:access:assign")}
onAuthRefresh={refreshAuth}
/>
)}
{!contributedSection && active === "system-roles" && <SystemRolesPanel settings={settings} canWrite={hasScope(auth, "system:roles:write")} onAuthRefresh={refreshAuth} />}
{!contributedSection && active === "system-audit" && <AdminAuditPanel settings={settings} auth={auth} systemMode />}
{!contributedSection && active === "tenant-users" && <UsersPanel settings={settings} auth={auth} canCreate={hasScope(auth, "admin:users:create")} canUpdate={hasScope(auth, "admin:users:update")} canSuspend={hasScope(auth, "admin:users:suspend")} canManageGroups={hasScope(auth, "admin:groups:manage_members")} canAssignRoles={hasScope(auth, "admin:roles:assign")} onAuthRefresh={refreshAuth} />}
{!contributedSection && active === "tenant-groups" && <GroupsPanel settings={settings} auth={auth} canDefine={hasScope(auth, "admin:groups:write")} canManageMembers={hasScope(auth, "admin:groups:manage_members")} canAssignRoles={hasScope(auth, "admin:roles:assign")} onAuthRefresh={refreshAuth} />}
@@ -185,6 +269,8 @@ export default function AdminPage({
{!contributedSection && active === "tenant-mail-servers" && <MailProfilesPanel settings={settings} scopeType="tenant" canWriteProfiles={hasScope(auth, "mail_servers:write")} canManageCredentials={hasScope(auth, "mail_servers:manage_credentials")} canWritePolicy={hasScope(auth, "admin:policies:write")} />}
{!contributedSection && active === "tenant-user-mail-servers" && <MailProfilesPanel settings={settings} scopeType="user" canWriteProfiles={hasScope(auth, "mail_servers:write")} canManageCredentials={hasScope(auth, "mail_servers:manage_credentials")} canWritePolicy={hasAnyScope(auth, ["admin:policies:write", "mail_servers:write"])} />}
{!contributedSection && active === "tenant-group-mail-servers" && <MailProfilesPanel settings={settings} scopeType="group" canWriteProfiles={hasScope(auth, "mail_servers:write")} canManageCredentials={hasScope(auth, "mail_servers:manage_credentials")} canWritePolicy={hasAnyScope(auth, ["admin:policies:write", "mail_servers:write"])} />}
{!contributedSection && active === "tenant-user-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="user" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
{!contributedSection && active === "tenant-group-file-connectors" && <FileConnectorsPanel settings={settings} scopeType="group" canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />}
{!contributedSection && active === "tenant-retention" && <RetentionPoliciesPanel settings={settings} scopeType="tenant" canWrite={hasScope(auth, "admin:policies:write")} />}
{!contributedSection && active === "tenant-user-retention" && <RetentionPoliciesPanel settings={settings} scopeType="user" canWrite={hasScope(auth, "admin:policies:write")} />}
{!contributedSection && active === "tenant-group-retention" && <RetentionPoliciesPanel settings={settings} scopeType="group" canWrite={hasScope(auth, "admin:policies:write")} />}
@@ -202,9 +288,14 @@ function canUseContributedSection(auth: AuthInfo, section: AdminSectionContribut
return true;
}
function contributedNavItems(sections: AdminSectionContribution[], available: ReadonlySet<string>, group: string): OrderedAdminNavItem[] {
function contributedNavItems(
sections: AdminSectionContribution[],
available: ReadonlySet<string>,
group: string,
excludedIds: ReadonlySet<string> = new Set()
): OrderedAdminNavItem[] {
return sections
.filter((section) => (section.group ?? "SYSTEM") === group && available.has(section.id))
.filter((section) => (section.group ?? "SYSTEM") === group && available.has(section.id) && !excludedIds.has(section.id))
.map((section) => ({ id: section.id, label: section.label, order: section.order ?? 100 }));
}