156 lines
4.5 KiB
TypeScript
156 lines
4.5 KiB
TypeScript
"use client";
|
|
import { useI18n } from "@/components/I18nProvider";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { createBrowserClient } from "@supabase/ssr";
|
|
import { passwordUpdateSchema } from "@/shared/account";
|
|
|
|
export function PasswordForm({ recovery = false }: { recovery?: boolean }) {
|
|
const { t } = useI18n();
|
|
const [current, setCurrent] = useState(""),
|
|
[password, setPassword] = useState(""),
|
|
[confirmation, setConfirmation] = useState(""),
|
|
[nonce, setNonce] = useState(""),
|
|
[needsNonce, setNeedsNonce] = useState(false),
|
|
[error, setError] = useState(""),
|
|
[message, setMessage] = useState(""),
|
|
[busy, setBusy] = useState(false);
|
|
const router = useRouter();
|
|
async function submit(event: React.FormEvent) {
|
|
event.preventDefault();
|
|
setError("");
|
|
setMessage("");
|
|
const body = {
|
|
password,
|
|
...(!recovery ? { currentPassword: current } : {}),
|
|
...(needsNonce ? { nonce } : {}),
|
|
};
|
|
const valid = passwordUpdateSchema.safeParse(body);
|
|
if (!valid.success) {
|
|
setError(valid.error.issues[0].message);
|
|
return;
|
|
}
|
|
if (password !== confirmation) {
|
|
setError("Your passwords do not match.");
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
try {
|
|
const response = await fetch("/api/v1/auth/password", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(valid.data),
|
|
signal: AbortSignal.timeout(30000),
|
|
});
|
|
const data = await response.json();
|
|
if (!response.ok) {
|
|
if (data.needsReauthentication) {
|
|
const client = createBrowserClient(
|
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
|
);
|
|
const { error } = await client.auth.reauthenticate();
|
|
if (error) throw error;
|
|
setNeedsNonce(true);
|
|
setMessage(
|
|
"Check your email for a verification code, then enter it below to finish.",
|
|
);
|
|
return;
|
|
}
|
|
throw new Error(data.error || "Could not update your password");
|
|
}
|
|
setCurrent("");
|
|
setPassword("");
|
|
setConfirmation("");
|
|
setNonce("");
|
|
setNeedsNonce(false);
|
|
setMessage(data.message);
|
|
if (recovery) {
|
|
router.replace("/settings?password=updated");
|
|
router.refresh();
|
|
}
|
|
} catch (e) {
|
|
setError((e as Error).message);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
return (
|
|
<form onSubmit={submit} aria-busy={busy} className="password-form">
|
|
{!recovery && (
|
|
<label className="field">
|
|
{t("Current password")}
|
|
<input
|
|
name="currentPassword"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
value={current}
|
|
onChange={(e) => setCurrent(e.target.value)}
|
|
required
|
|
disabled={busy}
|
|
/>
|
|
</label>
|
|
)}
|
|
<label className="field">
|
|
{t("New password")}
|
|
<input
|
|
name="newPassword"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
minLength={12}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
disabled={busy}
|
|
/>
|
|
<small>
|
|
{t("At least 12 characters. Use a unique password or passphrase.")}
|
|
</small>
|
|
</label>
|
|
<label className="field">
|
|
{t("Confirm new password")}
|
|
<input
|
|
name="confirmPassword"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
value={confirmation}
|
|
onChange={(e) => setConfirmation(e.target.value)}
|
|
required
|
|
disabled={busy}
|
|
/>
|
|
</label>
|
|
{needsNonce && (
|
|
<label className="field">
|
|
{t("Email verification code")}
|
|
<input
|
|
inputMode="numeric"
|
|
autoComplete="one-time-code"
|
|
value={nonce}
|
|
onChange={(e) => setNonce(e.target.value)}
|
|
required
|
|
disabled={busy}
|
|
/>
|
|
</label>
|
|
)}
|
|
{error && (
|
|
<p role="alert" className="error-message">
|
|
{t(error)}
|
|
</p>
|
|
)}
|
|
{message && (
|
|
<p role="status" className="success-message">
|
|
{t(message)}
|
|
</p>
|
|
)}
|
|
<button className="button primary" disabled={busy}>
|
|
{busy
|
|
? t("Updating…")
|
|
: recovery
|
|
? t("Set new password")
|
|
: t("Update password")}
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|