148 lines
4.3 KiB
TypeScript
148 lines
4.3 KiB
TypeScript
"use client";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { usePathname } from "next/navigation";
|
|
import { Copy, KeyRound } from "lucide-react";
|
|
import { useI18n } from "./I18nProvider";
|
|
import { api } from "@/lib/client-api";
|
|
import { generateInitialPassword } from "@/shared/initial-password";
|
|
|
|
export function LegacyPasswordNotice({ enabled }: { enabled: boolean }) {
|
|
const { t } = useI18n();
|
|
const pathname = usePathname();
|
|
const dialog = useRef<HTMLDialogElement>(null);
|
|
const [needed, setNeeded] = useState(false);
|
|
const [password, setPassword] = useState("");
|
|
const [busy, setBusy] = useState(false);
|
|
const [saved, setSaved] = useState(false);
|
|
const [copied, setCopied] = useState(false);
|
|
const [error, setError] = useState("");
|
|
useEffect(() => {
|
|
if (!enabled || pathname?.startsWith("/auth/") || password) return;
|
|
let active = true;
|
|
api<{ needed: boolean }>("auth/initial-password")
|
|
.then((data) => {
|
|
if (active) setNeeded(data.needed);
|
|
})
|
|
.catch(() => {});
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [enabled, pathname, password]);
|
|
useEffect(() => {
|
|
if (!needed) return;
|
|
const element = dialog.current;
|
|
element?.showModal();
|
|
return () => {
|
|
element?.close();
|
|
};
|
|
}, [needed]);
|
|
async function generate() {
|
|
if (busy) return;
|
|
setBusy(true);
|
|
setError("");
|
|
const generated = generateInitialPassword();
|
|
try {
|
|
await api("auth/initial-password", { password: generated });
|
|
// Only keep the confirmed password in this mounted dialog's memory.
|
|
setPassword(generated);
|
|
} catch (e) {
|
|
setError((e as Error).message);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
function dismiss() {
|
|
setNeeded(false);
|
|
setPassword("");
|
|
setSaved(false);
|
|
setCopied(false);
|
|
setError("");
|
|
}
|
|
async function copy() {
|
|
try {
|
|
await navigator.clipboard.writeText(password);
|
|
setCopied(true);
|
|
} catch {
|
|
setError("Select the password and copy it manually.");
|
|
}
|
|
}
|
|
if (!needed) return null;
|
|
return (
|
|
<dialog
|
|
ref={dialog}
|
|
className="report-dialog legacy-password-dialog"
|
|
aria-labelledby="legacy-password-title"
|
|
onCancel={(event) => {
|
|
if (password || busy) event.preventDefault();
|
|
else dismiss();
|
|
}}
|
|
>
|
|
<KeyRound size={28} aria-hidden="true" />
|
|
<h2 id="legacy-password-title">
|
|
{password
|
|
? t("Your one-time password screen.")
|
|
: t("Your account needs a password.")}
|
|
</h2>
|
|
<p>
|
|
{password
|
|
? t(
|
|
"Save this password now. After you close this screen, it cannot be shown again. You can change it in account settings.",
|
|
)
|
|
: t(
|
|
"You joined before passwords were required. Create a strong password for email-and-password login.",
|
|
)}
|
|
</p>
|
|
{password ? (
|
|
<>
|
|
<label className="field">
|
|
{t("Your new account password")}
|
|
<input
|
|
readOnly
|
|
value={password}
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
/>
|
|
</label>
|
|
<button className="button" onClick={copy}>
|
|
<Copy size={16} aria-hidden="true" />
|
|
{copied ? t("Copied") : t("Copy password")}
|
|
</button>
|
|
<label className="password-saved">
|
|
<input
|
|
type="checkbox"
|
|
checked={saved}
|
|
onChange={(e) => setSaved(e.target.checked)}
|
|
/>
|
|
{t("I saved this password somewhere safe.")}
|
|
</label>
|
|
<button
|
|
className="button primary full"
|
|
disabled={!saved}
|
|
onClick={dismiss}
|
|
>
|
|
{t("Done — I saved it")}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<button
|
|
className="button primary full"
|
|
disabled={busy}
|
|
onClick={generate}
|
|
>
|
|
{busy ? t("Creating password…") : t("Create and show my password")}
|
|
</button>
|
|
<button className="text-link" disabled={busy} onClick={dismiss}>
|
|
{t("Not now")}
|
|
</button>
|
|
</>
|
|
)}
|
|
{error && (
|
|
<p role="alert" className="error-message">
|
|
{t(error)}
|
|
</p>
|
|
)}
|
|
</dialog>
|
|
);
|
|
}
|