feat(access): add guarded local password lifecycle and recovery

This commit is contained in:
2026-09-08 07:47:18 +02:00
parent 8f8072b4ae
commit 7eb67b9fa7
31 changed files with 2377 additions and 33 deletions
+17 -4
View File
@@ -1,7 +1,10 @@
import { ContentGrid, DescriptionItem, DescriptionList, FormGrid } from "@govoplan/core-webui";
import { useEffect, useMemo, useRef, useState } from "react";
import { Search, Pencil, Plus, Trash2 } from "lucide-react";
import type { ApiSettings } from "@govoplan/core-webui";
import { Search, Pencil, Plus, Trash2, KeyRound } from "lucide-react";
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui";
import { hasScope } from "@govoplan/core-webui";
import PasswordRecoveryIssueDialog from "../passwords/PasswordRecoveryIssueDialog";
import { usePasswordPolicy } from "../passwords/usePasswordPolicy";
import { Button } from "@govoplan/core-webui";
import { ConfirmDialog } from "@govoplan/core-webui";
import { DataGrid, type DataGridColumn } from "@govoplan/core-webui";
@@ -36,6 +39,7 @@ const emptyDraft = {
export default function SystemUsersPanel({
settings,
auth,
canCreate,
canUpdate,
canSuspend,
@@ -50,7 +54,7 @@ export default function SystemUsersPanel({
}: {settings: ApiSettings;canCreate: boolean;canUpdate: boolean;canSuspend: boolean;canAssignRoles: boolean;canManageMemberships: boolean;onAuthRefresh: () => Promise<void>;}) {
}: {settings: ApiSettings;auth: AuthInfo;canCreate: boolean;canUpdate: boolean;canSuspend: boolean;canAssignRoles: boolean;canManageMemberships: boolean;onAuthRefresh: () => Promise<void>;}) {
const [accounts, setAccounts] = useState<SystemAccountItem[]>([]);
const [roles, setRoles] = useState<RoleSummary[]>([]);
const [tenants, setTenants] = useState<TenantAdminItem[]>([]);
@@ -61,6 +65,11 @@ export default function SystemUsersPanel({
const [viewing, setViewing] = useState<SystemAccountItem | null>(null);
const [deactivating, setDeactivating] = useState<SystemAccountItem | null>(null);
const [temporaryPassword, setTemporaryPassword] = useState<{email: string;value: string;} | null>(null);
const [recovering, setRecovering] = useState<SystemAccountItem | null>(null);
const { policy: passwordPolicy } = usePasswordPolicy(settings);
const canIssueRecovery = Boolean(passwordPolicy?.recovery_enabled
&& auth.principal?.auth_method === "session" && auth.user.local_password
&& hasScope(auth, "system:*") && !auth.user.required_auth_action);
const [draft, setDraft] = useState(emptyDraft);
const [savedDraftKey, setSavedDraftKey] = useState(draftKey(emptyDraft));
const [loading, setLoading] = useState(true);
@@ -216,13 +225,17 @@ export default function SystemUsersPanel({
{ id: "last_login", header: "i18n:govoplan-access.last_login.43dab84f", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.last_login_at || "", render: (row) => formatDateTime(row.last_login_at) },
{ id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[
{ id: "inspect", label: i18nMessage("i18n:govoplan-access.inspect_value.9d5d1071", { value0: row.email }), icon: <Search />, onClick: () => setViewing(row) },
...(canIssueRecovery && row.local_password === true && row.is_active
? [{ id: "recover-password", label: "i18n:govoplan-access.password.issue_title", icon: <KeyRound />, helpContextId: "access.password.issue-recovery", helpModuleId: "access", onClick: () => setRecovering(row) }]
: []),
{ id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.email }), icon: <Pencil />, disabled: !(canUpdate || canSuspend || canAssignRoles || canManageMemberships), disabledReason: !(canUpdate || canSuspend || canAssignRoles || canManageMemberships) ? ACCESS_INTERFACE_I18N.updatePermissionRequired : undefined, onClick: () => openEdit(row) },
{ id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.email }), icon: <Trash2 />, variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.memberships.some((membership) => membership.is_last_active_owner), disabledReason: !row.is_active ? "i18n:govoplan-access.inactive.09af574c" : !canSuspend ? ACCESS_INTERFACE_I18N.updatePermissionRequired : row.memberships.some((membership) => membership.is_last_active_owner) ? ACCESS_INTERFACE_I18N.lastOwnerCannotBeDeactivated : undefined, onClick: () => setDeactivating(row) }
]} /> }],
[canAssignRoles, canManageMemberships, canSuspend, canUpdate]);
[canAssignRoles, canManageMemberships, canSuspend, canUpdate, canIssueRecovery]);
return (
<>
{recovering && canIssueRecovery && <PasswordRecoveryIssueDialog key={recovering.account_id} settings={settings} account={recovering} onClose={() => setRecovering(null)} />}
<AdminPageLayout
title="i18n:govoplan-access.central_users.91ac1b51"
description="i18n:govoplan-access.global_login_identities_tenant_memberships_and_s.8f963b7f"