Add reset password page and update VendorsCard
Introduces a new password reset page with token validation and form handling. Removes the optional email field from the Vendor interface and its display in the VendorsCard component.
This commit is contained in:
157
app/auth/reset-password/page.tsx
Normal file
157
app/auth/reset-password/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { fetchClient } from "@/lib/api-client";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Vendor {
|
||||
id: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams.get('token');
|
||||
|
||||
const [vendor, setVendor] = useState<Vendor | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
toast.error("No reset token provided");
|
||||
router.push("/auth/login");
|
||||
return;
|
||||
}
|
||||
|
||||
validateToken();
|
||||
}, [token]);
|
||||
|
||||
async function validateToken() {
|
||||
try {
|
||||
const data = await fetchClient<{ valid: boolean; vendor?: Vendor; message?: string }>(`/auth/validate-reset-token/${token}`);
|
||||
|
||||
if (data.valid && data.vendor) {
|
||||
setVendor(data.vendor);
|
||||
} else {
|
||||
toast.error(data.message || "Invalid or expired token");
|
||||
router.push("/auth/login");
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error("Failed to validate token");
|
||||
router.push("/auth/login");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
toast.error("Passwords do not match");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
toast.error("Password must be at least 6 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
await fetchClient("/auth/reset-password", {
|
||||
method: "POST",
|
||||
body: { token, newPassword: password }
|
||||
});
|
||||
|
||||
toast.success("Password reset successfully");
|
||||
router.push("/auth/login");
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "Failed to reset password");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Validating token...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!vendor) {
|
||||
return null; // Will redirect
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="max-w-md w-full space-y-8 p-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold">Reset Password</h1>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Reset password for <span className="font-medium">{vendor.username}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium mb-2">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
|
||||
placeholder="Enter new password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium mb-2">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="w-full rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground disabled:opacity-60"
|
||||
>
|
||||
{submitting ? "Resetting..." : "Reset Password"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="text-center">
|
||||
<a
|
||||
href="/auth/login"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Back to Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { fetchClient } from "@/lib/api-client";
|
||||
interface Vendor {
|
||||
_id: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
isAdmin: boolean;
|
||||
createdAt: string;
|
||||
lastLogin?: string;
|
||||
@@ -63,7 +62,6 @@ export default function VendorsCard() {
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">{vendor.username}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{vendor.email && `${vendor.email} · `}
|
||||
Created: {new Date(vendor.createdAt).toLocaleDateString()}
|
||||
{vendor.lastLogin && ` · Last login: ${new Date(vendor.lastLogin).toLocaleDateString()}`}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user