mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-18 21:44:06 +01:00
feat: add password reset
This commit is contained in:
parent
d9415534fb
commit
6d9cc8a398
@ -1,3 +1,6 @@
|
||||
# NEXT
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||
|
||||
# Supabase Configuration
|
||||
PROJECT_ID=supabase-project-id
|
||||
NEXT_PUBLIC_SUPABASE_URL=supabase-project-url
|
||||
|
||||
81
src/app/auth/forgot/page.tsx
Normal file
81
src/app/auth/forgot/page.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Card, CardContent, CardFooter, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { ChevronLeft, CircleAlert } from "lucide-react";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [captchaToken, setCaptchaToken] = useState<string | undefined>(undefined);
|
||||
const captcha = useRef<HCaptcha | null>(null);
|
||||
|
||||
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!);
|
||||
|
||||
const forgotPassword = async (email: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
captchaToken,
|
||||
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL!}/reset`,
|
||||
});
|
||||
if (error) {
|
||||
toast.error("Failed to send reset link. Please check the email and try again.");
|
||||
} else {
|
||||
toast.success("Reset link sent! Check your email.");
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error("An unexpected error occurred. Please try again later.");
|
||||
} finally {
|
||||
captcha.current?.resetCaptcha();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container max-w-screen-xl flex items-center justify-center my-24">
|
||||
<Card>
|
||||
<CardHeader className="items-center">
|
||||
<CircleAlert className="text-red-600 w-12 h-12" />
|
||||
<CardTitle className="text-2xl font-bold">Forgot Password</CardTitle>
|
||||
<CardDescription>Enter your email and we'll send you a link to reset your password.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-y-2 justify-center items-center">
|
||||
{/* Input field with state binding */}
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="example@email.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<Button variant="default" className="w-full" onClick={() => forgotPassword(email)} disabled={loading}>
|
||||
{loading ? "Sending..." : "Send Reset Link"}
|
||||
</Button>
|
||||
<HCaptcha
|
||||
ref={captcha}
|
||||
sitekey={process.env.NEXT_PUBLIC_SITEKEY!}
|
||||
onVerify={(token) => {
|
||||
setCaptchaToken(token);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="text-xs justify-center">
|
||||
<Link href="/login" className="text-blue-600 hover:text-blue-800 flex justify-center items-center gap-x-2">
|
||||
<ChevronLeft /> Back to Login
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -23,6 +23,9 @@ export default function Login() {
|
||||
<LoginForm />
|
||||
<hr></hr>
|
||||
<LoginButton />
|
||||
<Link href={"/auth/forgot"}>
|
||||
<span className="text-blue-600 hover:text-blue-800 hover:underline">Forget your password?</span>
|
||||
</Link>
|
||||
</CardContent>
|
||||
<CardFooter className="text-xs justify-center">
|
||||
<span>
|
||||
|
||||
117
src/app/reset/page.tsx
Normal file
117
src/app/reset/page.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardFooter, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ListRestart } from "lucide-react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!);
|
||||
const router = useRouter();
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [resetSuccess, setResetSuccess] = useState(false);
|
||||
const [failedAttempts, setFailedAttempts] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetSuccess) return;
|
||||
|
||||
const { data: authListener } = supabase.auth.onAuthStateChange(async (event) => {
|
||||
if (event === "PASSWORD_RECOVERY") {
|
||||
toast.success("Password recovery successful! Please enter your new password.");
|
||||
setResetSuccess(true);
|
||||
setFailedAttempts(0);
|
||||
return;
|
||||
} else {
|
||||
setFailedAttempts((prev) => prev + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup listener on unmount
|
||||
return () => {
|
||||
authListener?.subscription.unsubscribe();
|
||||
};
|
||||
}, [resetSuccess, supabase.auth, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (failedAttempts >= 3) {
|
||||
router.push("/");
|
||||
}
|
||||
}, [failedAttempts, router]);
|
||||
|
||||
const updatePassword = async () => {
|
||||
if (!newPassword || !confirmPassword) {
|
||||
toast.error("Please fill out both fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
toast.error("Passwords do not match. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const { error } = await supabase.auth.updateUser({ password: newPassword });
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating password:", error);
|
||||
toast.error(error.message || "There was an error updating your password.");
|
||||
} else {
|
||||
toast.success("Password updated successfully!");
|
||||
router.push("/");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Unexpected error:", e);
|
||||
toast.error("An unexpected error occurred. Please try again.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container max-w-screen-xl flex items-center justify-center my-24">
|
||||
<Card>
|
||||
<CardHeader className="items-center">
|
||||
<ListRestart className="text-blue-600 w-12 h-12" />
|
||||
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
||||
<CardDescription>Enter your new password below to reset it.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-y-4 justify-center items-center">
|
||||
<Input
|
||||
id="new-password"
|
||||
type="password"
|
||||
placeholder="New password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
disabled={failedAttempts >= 3 && !resetSuccess}
|
||||
/>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
disabled={failedAttempts >= 3 && !resetSuccess}
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
className="w-full"
|
||||
onClick={updatePassword}
|
||||
disabled={loading || (failedAttempts >= 3 && !resetSuccess)}
|
||||
>
|
||||
{loading ? "Updating..." : "Update Password"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
<CardFooter className="text-xs justify-center">
|
||||
<p>If you encounter issues, please contact support.</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -14,6 +14,6 @@ export const config = {
|
||||
* - favicon.ico (favicon file)
|
||||
* Feel free to modify this pattern to include more paths.
|
||||
*/
|
||||
"/((?!_next/static|_next/image|$|favicon.ico|payment-success|verify|terms|contact|risks|about|privacy|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
||||
"/((?!_next/static|_next/image|$|favicon.ico|payment-success|verify|terms|contact|risks|about|privacy|reset|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
||||
],
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user