diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index caea6a7..6a67346 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,17 +1,24 @@ 'use client' import { useState } from 'react' -import { useRouter } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import Link from 'next/link' import { createClient } from '@/lib/supabase/client' export default function LoginPage() { const router = useRouter() + const searchParams = useSearchParams() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState(null) const [loading, setLoading] = useState(false) + // Check for success message from password reset + const message = searchParams.get('message') + const successMessage = message === 'password_reset_success' + ? 'Your password has been reset successfully. Please sign in with your new password.' + : null + async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError(null) @@ -46,6 +53,12 @@ export default function LoginPage() {
+ {successMessage && ( +
+

{successMessage}

+
+ )} + {error && (

{error}

diff --git a/src/app/reset-password/page.tsx b/src/app/reset-password/page.tsx new file mode 100644 index 0000000..37d4eb7 --- /dev/null +++ b/src/app/reset-password/page.tsx @@ -0,0 +1,218 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { createClient } from '@/lib/supabase/client' + +export default function ResetPasswordPage() { + const router = useRouter() + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + const [tokenValid, setTokenValid] = useState(null) + + useEffect(() => { + // Handle recovery token from URL hash + // Supabase adds tokens to the URL hash after redirect from reset email + const handleTokenFromUrl = async () => { + const hash = window.location.hash + if (hash) { + const params = new URLSearchParams(hash.substring(1)) + const accessToken = params.get('access_token') + const refreshToken = params.get('refresh_token') + const type = params.get('type') + + if (accessToken && refreshToken && type === 'recovery') { + const supabase = createClient() + const { error } = await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }) + + if (error) { + setError('Invalid or expired reset link. Please request a new password reset.') + setTokenValid(false) + return + } + + setTokenValid(true) + } else { + setError('Invalid or expired reset link. Please request a new password reset.') + setTokenValid(false) + } + } else { + // No hash in URL - check if user already has a session from a previous recovery + const supabase = createClient() + const { data: { session } } = await supabase.auth.getSession() + + if (session) { + setTokenValid(true) + } else { + setError('Invalid or expired reset link. Please request a new password reset.') + setTokenValid(false) + } + } + } + + handleTokenFromUrl() + }, []) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setError(null) + + // Validate passwords match + if (password !== confirmPassword) { + setError('Passwords do not match') + return + } + + // Validate password length + if (password.length < 6) { + setError('Password must be at least 6 characters') + return + } + + setLoading(true) + + const supabase = createClient() + + const { error: updateError } = await supabase.auth.updateUser({ + password, + }) + + if (updateError) { + setError(updateError.message) + setLoading(false) + return + } + + // Sign out after password reset so user can sign in with new password + await supabase.auth.signOut() + + // Redirect to login with success message + router.push('/login?message=password_reset_success') + } + + // Show loading state while checking token + if (tokenValid === null) { + return ( +
+
+

Verifying reset link...

+
+
+ ) + } + + // Show error state if token is invalid + if (tokenValid === false) { + return ( +
+
+
+

+ Reset link expired +

+

+ {error} +

+
+ +
+ + Request a new reset link + +
+
+
+ ) + } + + return ( +
+
+
+

+ Set new password +

+

+ Enter your new password below. +

+
+ + + {error && ( +
+

{error}

+
+ )} + +
+
+ + setPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="••••••••" + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-zinc-900 placeholder-zinc-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-50 dark:placeholder-zinc-500" + placeholder="••••••••" + /> +
+
+ + + +
+ + Back to sign in + +
+ +
+
+ ) +}