180 lines
7.2 KiB
TypeScript
180 lines
7.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Copy, Dice5, RefreshCw } from "lucide-react";
|
|
import Button from "./Button";
|
|
import Dialog from "./Dialog";
|
|
import DismissibleAlert from "./DismissibleAlert";
|
|
import FormField from "./FormField";
|
|
import IconButton from "./IconButton";
|
|
import ToggleSwitch from "./ToggleSwitch";
|
|
import {
|
|
DEFAULT_PASSWORD_GENERATOR_OPTIONS,
|
|
generateSecurePassword,
|
|
PasswordGeneratorError,
|
|
type PasswordGeneratorErrorCode,
|
|
type PasswordGeneratorOptions
|
|
} from "./passwordGenerator";
|
|
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
|
import type { PlatformInterfaceIdentityProps } from "../types";
|
|
|
|
const GENERATION_ERROR_LABELS: Record<PasswordGeneratorErrorCode, string> = {
|
|
"invalid-length": "i18n:govoplan-core.password_length_must_be_between_12_and_128_character.4d147c07",
|
|
"no-character-set": "i18n:govoplan-core.select_at_least_one_character_set.6150a9c2",
|
|
"length-too-short": "i18n:govoplan-core.password_length_is_too_short_for_the_selected_charact.1d7845f1",
|
|
"invalid-random-range": "i18n:govoplan-core.password_generation_could_not_use_the_selected_setti.c2f0da38",
|
|
"secure-random-unavailable": "i18n:govoplan-core.secure_browser_password_generation_is_unavailable.55275f10"
|
|
};
|
|
|
|
export type PasswordGeneratorDialogProps = PlatformInterfaceIdentityProps & {
|
|
open: boolean;
|
|
initialOptions?: Partial<PasswordGeneratorOptions>;
|
|
onUse: (password: string) => void;
|
|
onClose: () => void;
|
|
};
|
|
|
|
export default function PasswordGeneratorDialog({
|
|
open,
|
|
initialOptions,
|
|
helpContextId,
|
|
helpModuleId,
|
|
helpTopicId,
|
|
onUse,
|
|
onClose
|
|
}: PasswordGeneratorDialogProps) {
|
|
const { translateText } = usePlatformLanguage();
|
|
const [options, setOptions] = useState<PasswordGeneratorOptions>({
|
|
...DEFAULT_PASSWORD_GENERATOR_OPTIONS,
|
|
...initialOptions
|
|
});
|
|
const [candidate, setCandidate] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const generate = () => {
|
|
try {
|
|
setCandidate(generateSecurePassword(options));
|
|
setError("");
|
|
setCopied(false);
|
|
} catch (generationError) {
|
|
setCandidate("");
|
|
setError(translateText(
|
|
generationError instanceof PasswordGeneratorError
|
|
? GENERATION_ERROR_LABELS[generationError.code]
|
|
: "i18n:govoplan-core.password_generation_failed.5a40a004"
|
|
));
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
setOptions({ ...DEFAULT_PASSWORD_GENERATOR_OPTIONS, ...initialOptions });
|
|
}, [initialOptions, open]);
|
|
|
|
useEffect(() => {
|
|
if (open) generate();
|
|
// Generation must react to the exact selected candidate policy.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [open, options]);
|
|
|
|
const setOption = <K extends keyof PasswordGeneratorOptions>(
|
|
key: K,
|
|
value: PasswordGeneratorOptions[K]
|
|
) => setOptions((current) => ({ ...current, [key]: value }));
|
|
|
|
const copy = async () => {
|
|
if (!candidate || typeof navigator === "undefined" || !navigator.clipboard?.writeText) return;
|
|
try {
|
|
await navigator.clipboard.writeText(candidate);
|
|
setCopied(true);
|
|
} catch {
|
|
setCopied(false);
|
|
setError(translateText("i18n:govoplan-core.the_generated_password_could_not_be_copied_select_it.569e025e"));
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
title="i18n:govoplan-core.generate_password.bd5bede8"
|
|
className="password-generator-dialog"
|
|
bodyClassName="password-generator-body"
|
|
footerClassName="button-row compact-actions"
|
|
portal
|
|
helpContextId={helpContextId}
|
|
helpModuleId={helpModuleId}
|
|
helpTopicId={helpTopicId}
|
|
onClose={onClose}
|
|
footer={(
|
|
<>
|
|
<Button type="button" onClick={onClose}>i18n:govoplan-core.cancel.77dfd213</Button>
|
|
<Button
|
|
type="button"
|
|
variant="primary"
|
|
disabled={!candidate}
|
|
helpContextId={helpContextId}
|
|
helpModuleId={helpModuleId}
|
|
helpTopicId={helpTopicId}
|
|
onClick={() => {
|
|
if (!candidate) return;
|
|
onUse(candidate);
|
|
onClose();
|
|
}}
|
|
>
|
|
<Dice5 size={16} aria-hidden="true" /> i18n:govoplan-core.use_password.2e1913a6
|
|
</Button>
|
|
</>
|
|
)}
|
|
>
|
|
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
|
<div className="password-generator-options">
|
|
<FormField label="i18n:govoplan-core.length.adc95605" helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId}>
|
|
<input
|
|
type="number"
|
|
min={12}
|
|
max={128}
|
|
step={1}
|
|
value={options.length}
|
|
onChange={(event) => setOption("length", Number(event.target.value))}
|
|
/>
|
|
</FormField>
|
|
<div className="password-generator-character-sets" aria-label={translateText("i18n:govoplan-core.character_sets.db6efda2")}>
|
|
<ToggleSwitch label="i18n:govoplan-core.lowercase.3b677a18" checked={options.lowercase} onChange={(checked) => setOption("lowercase", checked)} helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId} />
|
|
<ToggleSwitch label="i18n:govoplan-core.uppercase.b463d690" checked={options.uppercase} onChange={(checked) => setOption("uppercase", checked)} helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId} />
|
|
<ToggleSwitch label="i18n:govoplan-core.digits.9cd500d3" checked={options.digits} onChange={(checked) => setOption("digits", checked)} helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId} />
|
|
<ToggleSwitch label="i18n:govoplan-core.symbols.9491fc41" checked={options.symbols} onChange={(checked) => setOption("symbols", checked)} helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId} />
|
|
</div>
|
|
</div>
|
|
<FormField label="i18n:govoplan-core.generated_password.78461854" helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId}>
|
|
<div className="password-generator-result">
|
|
<input
|
|
type="text"
|
|
value={candidate}
|
|
readOnly
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
/>
|
|
<IconButton
|
|
label={copied ? "i18n:govoplan-core.copied.8d525e5f" : "i18n:govoplan-core.copy_generated_password.180ca876"}
|
|
icon={<Copy size={16} />}
|
|
onClick={() => void copy()}
|
|
disabled={!candidate || typeof navigator === "undefined" || !navigator.clipboard?.writeText}
|
|
helpContextId={helpContextId}
|
|
helpModuleId={helpModuleId}
|
|
helpTopicId={helpTopicId}
|
|
/>
|
|
<IconButton
|
|
label="i18n:govoplan-core.generate_another_password.d99fc019"
|
|
icon={<RefreshCw size={16} />}
|
|
onClick={generate}
|
|
helpContextId={helpContextId}
|
|
helpModuleId={helpModuleId}
|
|
helpTopicId={helpTopicId}
|
|
/>
|
|
</div>
|
|
</FormField>
|
|
<p className="password-generator-note">
|
|
i18n:govoplan-core.the_current_field_is_not_changed_until_you_choose_use_pass.447608e7
|
|
</p>
|
|
</Dialog>
|
|
);
|
|
}
|