mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-20 14:34:05 +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
|
# Supabase Configuration
|
||||||
PROJECT_ID=supabase-project-id
|
PROJECT_ID=supabase-project-id
|
||||||
NEXT_PUBLIC_SUPABASE_URL=supabase-project-url
|
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 />
|
<LoginForm />
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
<LoginButton />
|
<LoginButton />
|
||||||
|
<Link href={"/auth/forgot"}>
|
||||||
|
<span className="text-blue-600 hover:text-blue-800 hover:underline">Forget your password?</span>
|
||||||
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="text-xs justify-center">
|
<CardFooter className="text-xs justify-center">
|
||||||
<span>
|
<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)
|
* - favicon.ico (favicon file)
|
||||||
* Feel free to modify this pattern to include more paths.
|
* 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