diff --git a/.env.example b/.env.example index c56c30f..c3e0411 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/src/app/auth/forgot/page.tsx b/src/app/auth/forgot/page.tsx new file mode 100644 index 0000000..b1a616b --- /dev/null +++ b/src/app/auth/forgot/page.tsx @@ -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(undefined); + const captcha = useRef(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 ( +
+ + + + Forgot Password + Enter your email and we'll send you a link to reset your password. + + + {/* Input field with state binding */} + setEmail(e.target.value)} + /> + + { + setCaptchaToken(token); + }} + /> + + + + Back to Login + + + +
+ ); +} diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index bc663aa..f644a04 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -23,6 +23,9 @@ export default function Login() {
+ + Forget your password? + diff --git a/src/app/reset/page.tsx b/src/app/reset/page.tsx new file mode 100644 index 0000000..3e08bc8 --- /dev/null +++ b/src/app/reset/page.tsx @@ -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 ( +
+ + + + Reset Password + Enter your new password below to reset it. + + + setNewPassword(e.target.value)} + disabled={failedAttempts >= 3 && !resetSuccess} + /> + setConfirmPassword(e.target.value)} + disabled={failedAttempts >= 3 && !resetSuccess} + /> + + + +

If you encounter issues, please contact support.

+
+
+
+ ); +} diff --git a/src/middleware.ts b/src/middleware.ts index def9ff9..72d726d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -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)$).*)", ], };