feat(access): add guarded local password lifecycle and recovery
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { useId, useState, type FormEvent } from "react";
|
||||
import {
|
||||
Button, Dialog, DismissibleAlert, FormField, FormLayout, PasswordField,
|
||||
i18nMessage, usePlatformLanguage, type ApiSettings
|
||||
} from "@govoplan/core-webui";
|
||||
import { issuePasswordRecovery, passwordErrorMessage, type PasswordRecoveryCode } from "../../api/passwords";
|
||||
|
||||
export default function PasswordRecoveryIssueDialog({ settings, account, onClose }: {
|
||||
settings: ApiSettings;
|
||||
account: { account_id: string; email: string };
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const formId = useId();
|
||||
const [password, setPassword] = useState("");
|
||||
const [verified, setVerified] = useState(false);
|
||||
const [result, setResult] = useState<PasswordRecoveryCode | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const ready = verified && Boolean(password) && Array.from(password).length <= 1024;
|
||||
|
||||
async function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (busy || !ready || result) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
setResult(await issuePasswordRecovery(settings, account.account_id, password, true));
|
||||
} catch (reason) { setError(passwordErrorMessage(reason)); }
|
||||
finally {
|
||||
setPassword("");
|
||||
setVerified(false);
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return <Dialog open variant="administration" size="large"
|
||||
title="i18n:govoplan-access.password.issue_title" onClose={() => { if (!busy) onClose(); }}
|
||||
footer={<>
|
||||
<Button onClick={onClose} disabled={busy} helpContextId="access.password.issue-recovery" helpModuleId="access">{result ? "i18n:govoplan-access.close.bbfa773e" : "i18n:govoplan-access.cancel.77dfd213"}</Button>
|
||||
{!result && <Button type="submit" form={formId} variant="primary" disabled={busy || !ready} helpContextId="access.password.issue-recovery" helpModuleId="access"
|
||||
disabledReason={busy ? "i18n:govoplan-access.password.saving" : !ready ? "i18n:govoplan-access.password.issue_requirements" : undefined}>
|
||||
i18n:govoplan-access.password.issue_title
|
||||
</Button>}
|
||||
</>}>
|
||||
<p>{i18nMessage("i18n:govoplan-access.password.issue_for", { value0: account.email })}</p>
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||
{result ? <>
|
||||
<p>i18n:govoplan-access.password.code_once</p>
|
||||
<FormField label="i18n:govoplan-access.password.recovery_code" helpContextId="access.password.issue-recovery" helpModuleId="access"><input value={result.recovery_code} readOnly autoComplete="off" /></FormField>
|
||||
<p>{i18nMessage("i18n:govoplan-access.password.code_expires", { value0: new Date(result.expires_at).toLocaleString() })}</p>
|
||||
<p>i18n:govoplan-access.password.code_delivery</p>
|
||||
</> : <FormLayout columns={1} collapseAt="standard" id={formId} onSubmit={submit}>
|
||||
<p>i18n:govoplan-access.password.issue_consequences</p>
|
||||
<FormField label="i18n:govoplan-access.current_password.5e551021" helpContextId="access.password.issue-recovery" helpModuleId="access"><PasswordField aria-label={translateText("i18n:govoplan-access.current_password.5e551021")} value={password} onValueChange={setPassword} autoComplete="current-password" maxLength={2048} required disabled={busy} /></FormField>
|
||||
<label className="checkbox-field" data-help-context-id="access.password.issue-recovery" data-help-module-id="access"><input type="checkbox" checked={verified} onChange={(event) => setVerified(event.target.checked)} disabled={busy} required /> <span>i18n:govoplan-access.password.identity_verified</span></label>
|
||||
</FormLayout>}
|
||||
</Dialog>;
|
||||
}
|
||||
Reference in New Issue
Block a user