From 75339228f3581f7ce5ecd16f00723deab8d07e96 Mon Sep 17 00:00:00 2001 From: Lyubomir Stoykov Date: Sat, 6 Dec 2025 10:16:20 +0200 Subject: [PATCH 1/2] Add password reset functionality to email/password auth - Add password reset request flow with email sending - Add password update flow after recovery link is clicked - Handle recovery params from URL hash and query string - Add UI states for reset request and password update modes - Add 'Forgot password?' link on sign-in form - Clean up console.log statements --- app/email-password/EmailPasswordDemo.tsx | 294 +++++++++++++++++++---- app/email-password/page.tsx | 1 - app/google-login/page.tsx | 2 +- proxy.ts | 1 - 4 files changed, 244 insertions(+), 54 deletions(-) diff --git a/app/email-password/EmailPasswordDemo.tsx b/app/email-password/EmailPasswordDemo.tsx index e9b4e7c..2bb865a 100644 --- a/app/email-password/EmailPasswordDemo.tsx +++ b/app/email-password/EmailPasswordDemo.tsx @@ -9,13 +9,33 @@ type EmailPasswordDemoProps = { user: User | null; }; -type Mode = "signup" | "signin" +type Mode = "signup" | "signin"; +type ResetMode = "none" | "request" | "update"; + +function hasRecoveryParams() { + if (typeof window === "undefined") { + return false; + } + const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, "")); + if (hashParams.get("type") === "recovery") { + return true; + } + const searchParams = new URLSearchParams(window.location.search); + return searchParams.get("password-reset") === "true"; +} export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { const [mode, setMode] = useState("signup"); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); - const [status, setStatus] = useState(""); + const [status, setStatus] = useState(() => + hasRecoveryParams() ? "Enter a new password to finish resetting your account." : "" + ); + const [resetMode, setResetMode] = useState(() => + hasRecoveryParams() ? "update" : "none" + ); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); const supabase = getSupabaseBrowserClient(); const [currentUser, setCurrentUser] = useState(user); @@ -23,25 +43,50 @@ export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { await supabase.auth.signOut(); setCurrentUser(null); setStatus("Signed out successfully"); + setResetMode("none"); + setNewPassword(""); + setConfirmPassword(""); } useEffect(() => { const { data: listener } = supabase.auth.onAuthStateChange( - (_event, session) => { + (event, session) => { setCurrentUser(session?.user ?? null); + if (event === "PASSWORD_RECOVERY") { + setResetMode("update"); + setStatus("Enter a new password to finish resetting your account."); + } } ); return () => { listener?.subscription.unsubscribe(); }; - }, [supabase]) + }, [supabase]); + + useEffect(() => { + if (!hasRecoveryParams() || typeof window === "undefined") { + return; + } + setResetMode("update"); + setStatus("Enter a new password to finish resetting your account."); + + const searchParams = new URLSearchParams(window.location.search.replace(/^\?/, "")); + if (searchParams.has("password-reset")) { + searchParams.delete("password-reset"); + } + window.history.replaceState( + null, + document.title, + `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}` + ); + }, []); async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (mode == "signup") { - const { error, data } = await supabase.auth.signUp({ + const { error } = await supabase.auth.signUp({ email, password, options: { @@ -54,7 +99,7 @@ export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { setStatus("Check your inbox to confirm the new account."); } } else { - const { error, data } = await supabase.auth.signInWithPassword({ + const { error } = await supabase.auth.signInWithPassword({ email, password, }); @@ -66,6 +111,89 @@ export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { } } + async function handlePasswordResetRequest(event: React.FormEvent) { + event.preventDefault(); + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${window.location.origin}/email-password?password-reset=true`, + }); + + if (error) { + setStatus(error.message); + return; + } + + setStatus("Password reset email sent. Check your inbox."); + setResetMode("none"); + setMode("signin"); + } + + async function handlePasswordUpdate(event: React.FormEvent) { + event.preventDefault(); + if (newPassword.length < 6) { + setStatus("New password must be at least 6 characters."); + return; + } + if (newPassword !== confirmPassword) { + setStatus("Passwords do not match."); + return; + } + + const { error } = await supabase.auth.updateUser({ password: newPassword }); + if (error) { + setStatus(error.message); + return; + } + + setStatus("Password updated. You're now signed in."); + setResetMode("none"); + setNewPassword(""); + setConfirmPassword(""); + } + + function startPasswordReset() { + setResetMode("request"); + setStatus(""); + setMode("signin"); + setPassword(""); + } + + function cancelPasswordReset() { + setResetMode("none"); + setMode("signin"); + setStatus(""); + setNewPassword(""); + setConfirmPassword(""); + setPassword(""); + } + + const isResetRequest = resetMode === "request"; + const isPasswordUpdate = resetMode === "update"; + const handleFormSubmit = isResetRequest + ? handlePasswordResetRequest + : isPasswordUpdate + ? handlePasswordUpdate + : handleSubmit; + const primaryButtonLabel = isResetRequest + ? "Send reset email" + : isPasswordUpdate + ? "Update password" + : mode === "signup" + ? "Create account" + : "Sign in"; + const primaryHeading = isResetRequest + ? "Request password reset" + : isPasswordUpdate + ? "Choose a new password" + : mode === "signup" + ? "Create an account" + : "Welcome back"; + const primarySubheading = isResetRequest + ? "Enter your account email and we'll send recovery instructions." + : isPasswordUpdate + ? "Supabase opened a recovery session—set a new password to finish." + : null; + const badgeLabel = isResetRequest || isPasswordUpdate ? "Recovery" : "Credentials"; + return ( - {!currentUser && ( + {(!currentUser || resetMode !== "none") && ( <>

- Credentials + {badgeLabel}

- {mode === "signup" ? "Create an account" : "Welcome back"} + {primaryHeading}

+ {primarySubheading && ( +

+ {primarySubheading} +

+ )}
-
- {(["signup", "signin"] as Mode[]).map((option) => ( - - ))} -
+ {resetMode === "none" ? ( +
+ {(["signup", "signin"] as Mode[]).map((option) => ( + + ))} +
+ ) : ( + + )}
- - + {resetMode !== "update" && ( + + )} + {resetMode === "none" && ( + + )} + {isPasswordUpdate && ( + <> + + + + )}
+ {resetMode === "none" && mode === "signin" && ( + + )} {status && (

{status} @@ -213,4 +405,4 @@ export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { ); -} \ No newline at end of file +} diff --git a/app/email-password/page.tsx b/app/email-password/page.tsx index a928fa5..8fbb955 100644 --- a/app/email-password/page.tsx +++ b/app/email-password/page.tsx @@ -7,6 +7,5 @@ export default async function EmailPasswordPage() { data: { user }, } = await supabase.auth.getUser(); - console.log( { user }); return ; } \ No newline at end of file diff --git a/app/google-login/page.tsx b/app/google-login/page.tsx index 46a2b94..60134f7 100644 --- a/app/google-login/page.tsx +++ b/app/google-login/page.tsx @@ -7,6 +7,6 @@ export default async function GoogleLoginPage() { data: { user }, } = await supabase.auth.getUser(); - console.log( { user }); + return ; } \ No newline at end of file diff --git a/proxy.ts b/proxy.ts index 7c5f99f..a7055bd 100644 --- a/proxy.ts +++ b/proxy.ts @@ -24,7 +24,6 @@ export async function proxy(request: NextRequest) { const { data: { user }, } = await supabase.auth.getUser(); - console.log ({ user }); // Redirect non-authenticated users away from protected routes if (!user && request.nextUrl.pathname.startsWith("/protected")) { From 9e0b7ea6b08daaf1078bbbdb30cf9b3945d27415 Mon Sep 17 00:00:00 2001 From: Lyubomir Stoykov Date: Sat, 6 Dec 2025 10:17:18 +0200 Subject: [PATCH 2/2] Add password reset flow documentation to README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 67d8967..d936a42 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,17 @@ Open [http://localhost:3000](http://localhost:3000) to see the demo. - `/email-password` - Email + Password authentication demo - `/google-login` - Google OAuth authentication demo +## Password Reset Flow + +The email/password demo ships with a complete Supabase recovery experience: + +1. Click **Forgot password?** while in the sign-in view. The client calls `supabase.auth.resetPasswordForEmail` and sends users to `/email-password?password-reset=true`. +2. Supabase emails the reset link. Configure the Auth **Site URL** and allowed **Redirect URLs** in your project so the link is accepted in local/dev/staging environments. +3. Following the link both signs the user in and flags the recovery session. The page detects `#type=recovery` or the `password-reset` query and surfaces the “Choose a new password” form even though a session exists. +4. Submitting that form calls `supabase.auth.updateUser({ password })` and keeps the new session active so users stay logged in. + +If you customize the route, update the `redirectTo` option and allowed redirect URLs in Supabase to match. + ## Scripts - `npm run dev` - Start development server