Files
MineGeo/components/LegacyPasswordNotice.tsx

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>
);
}