109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useState } from "react";
|
|
import { useSearchParams, useRouter } from "next/navigation";
|
|
import { AuthCard, AuthFooterLink } from "@/components/auth/auth-card";
|
|
import { auth, AuthError } from "@/lib/auth";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
function ResetPasswordForm() {
|
|
const params = useSearchParams();
|
|
const router = useRouter();
|
|
const token = params.get("token") ?? "";
|
|
|
|
const [password, setPassword] = useState("");
|
|
const [confirm, setConfirm] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [done, setDone] = useState(false);
|
|
|
|
if (!token) {
|
|
return (
|
|
<AuthCard title="Invalid reset link" description="The link is missing a token. Please request a new one.">
|
|
<AuthFooterLink prefix="Need a new link?" href="/forgot-password" label="Request one" />
|
|
</AuthCard>
|
|
);
|
|
}
|
|
|
|
async function onSubmit(e: React.SyntheticEvent) {
|
|
e.preventDefault();
|
|
setError(null);
|
|
if (password !== confirm) {
|
|
setError("Passwords do not match.");
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
try {
|
|
await auth.resetPassword(token, password);
|
|
setDone(true);
|
|
setTimeout(() => router.push("/login"), 1500);
|
|
} catch (err) {
|
|
setError(err instanceof AuthError ? err.message : "Reset failed.");
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
if (done) {
|
|
return (
|
|
<AuthCard
|
|
title="Password updated"
|
|
description="Redirecting you to the login page…"
|
|
>
|
|
<AuthFooterLink prefix="Take me there now —" href="/login" label="Log in" />
|
|
</AuthCard>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthCard
|
|
title="Set a new password"
|
|
description="Choose a strong password you haven't used before."
|
|
>
|
|
<form onSubmit={onSubmit} className="space-y-6">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="password">New password</Label>
|
|
<Input
|
|
id="password"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
minLength={8}
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="confirm">Confirm password</Label>
|
|
<Input
|
|
id="confirm"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
minLength={8}
|
|
required
|
|
value={confirm}
|
|
onChange={(e) => setConfirm(e.target.value)}
|
|
/>
|
|
</div>
|
|
<br />
|
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
|
<Button type="submit" disabled={submitting} className="w-full h-11 text-base">
|
|
{submitting ? "Updating…" : "Update password"}
|
|
</Button>
|
|
</form>
|
|
</AuthCard>
|
|
);
|
|
}
|
|
|
|
export default function ResetPasswordPage() {
|
|
// useSearchParams must be inside Suspense for Next.js streaming.
|
|
return (
|
|
<Suspense fallback={null}>
|
|
<ResetPasswordForm />
|
|
</Suspense>
|
|
);
|
|
}
|