diff --git a/frontend-nextjs/src/app/api/auth/forgot-password/route.ts b/frontend-nextjs/src/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..4b804d4 --- /dev/null +++ b/frontend-nextjs/src/app/api/auth/forgot-password/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from 'next/server' +import { isValidEmail } from '@/backend/core/shared/validation' +import { forgotPassword } from '@/backend/core/user/UserService'; + +/** + * API route handler for initiating password reset process + * Sends a password reset email with a recovery token to the user + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { email } = body; + + // Validate required fields + if (!isValidEmail(email)) { + return NextResponse.json({ error: 'Invalid email address' }, { status: 400 }); + } + + const result = await forgotPassword(email); + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + message: result.message || 'Password reset email sent successfully', + }, { status: 200 }); + + } catch (error) { + console.error('Forgot password error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/frontend-nextjs/src/app/api/auth/reset-password/route.ts b/frontend-nextjs/src/app/api/auth/reset-password/route.ts new file mode 100644 index 0000000..a82dc19 --- /dev/null +++ b/frontend-nextjs/src/app/api/auth/reset-password/route.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from 'next/server' +import { isValidPassword } from '@/backend/core/shared/validation' +import { resetPassword } from '@/backend/core/user/UserService'; + +/** + * API route handler for password reset with recovery token + * This endpoint receives the recovery token from the password reset email + * and updates the user's password after validating the token + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { accessToken, refreshToken, newPassword } = body; + + // Validate required fields + if (!accessToken) { + return NextResponse.json({ error: 'Recovery token is required' }, { status: 400 }); + } + + if (!isValidPassword(newPassword)) { + return NextResponse.json({ error: 'Password must be at least 8 characters and include a number' }, { status: 400 }); + } + + const result = await resetPassword(accessToken, refreshToken || '', newPassword); + + if (!result.success) { + return NextResponse.json({ error: result.error }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + message: result.message || 'Password reset successfully', + }, { status: 200 }); + + } catch (error) { + console.error('Reset password error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/frontend-nextjs/src/app/auth/forgot-password/page.tsx b/frontend-nextjs/src/app/auth/forgot-password/page.tsx index 9554cc6..b9bd785 100644 --- a/frontend-nextjs/src/app/auth/forgot-password/page.tsx +++ b/frontend-nextjs/src/app/auth/forgot-password/page.tsx @@ -43,21 +43,32 @@ export default function ForgotPasswordPage() { } setIsLoading(true); - // Submit registration data to backend API + // Submit forgot password request to backend API try { - const result = { success: true, error: null, data: null }; // Placeholder for actual API call - if (!result.success) { - setGeneralError(result.error || "Forgot password process failed. Please try again."); + const response = await fetch('/api/auth/forgot-password', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email: formData.email }), + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + setGeneralError(result.error || "Failed to send password reset email. Please try again."); } else { - console.log("Forgot password process successful:"); - // Show confirmation toast + console.log("Password reset email sent successfully"); + setGeneralError(null); + // Show success message + alert("Password reset email sent! Please check your inbox."); } } catch (submissionError) { setGeneralError("An error occurred during forgot password process. Please try again."); + } finally { + setIsLoading(false); } - setTimeout(() => setIsLoading(false), 1000); // Simulate network delay - // setIsLoading(false); }; diff --git a/frontend-nextjs/src/app/auth/reset-password/page.tsx b/frontend-nextjs/src/app/auth/reset-password/page.tsx index a3ebaf2..34ec3ee 100644 --- a/frontend-nextjs/src/app/auth/reset-password/page.tsx +++ b/frontend-nextjs/src/app/auth/reset-password/page.tsx @@ -1,10 +1,9 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import Link from "next/link"; import { isValidPassword } from "@/utils/validation"; import { PasswordInput } from "@/components/ui/Inputs"; import { AuthSubmitButton } from "@/components/ui/Buttons"; -import { useSearchParams } from "next/dist/client/components/navigation"; interface FieldErrors { @@ -21,9 +20,35 @@ export default function ResetPasswordPage() { const [fieldErrors, setFieldErrors] = useState({}); const [generalError, setGeneralError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [accessToken, setAccessToken] = useState(null); + const [refreshToken, setRefreshToken] = useState(null); + + // Extract tokens from URL hash fragment (Supabase sends tokens in URL hash) + useEffect(() => { + if (typeof window !== 'undefined') { + const hashParams = new URLSearchParams(window.location.hash.substring(1)); + const access = hashParams.get('access_token'); + const refresh = hashParams.get('refresh_token'); + + if (access) { + setAccessToken(access); + setRefreshToken(refresh || ''); + } + } + }, []); - const searchParams = useSearchParams(); - const accessToken = searchParams.get('access_token'); + if (accessToken === null) { + return ( +
+
+
+

Loading...

+

Please wait while we verify your reset link.

+
+
+
+ ); + } if (!accessToken) { return ( @@ -75,22 +100,37 @@ export default function ResetPasswordPage() { } setIsLoading(true); - // Submit registration data to backend API + // Submit password reset to backend API try { - const result = { success: true, error : null, data: null }; // Placeholder for actual API call + const response = await fetch('/api/auth/reset-password', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + accessToken: accessToken, + refreshToken: refreshToken, + newPassword: formData.password + }), + }); + + const result = await response.json(); - if (!result.success) { - setGeneralError(result.error || "Reset password failed. Please try again."); + if (!response.ok || !result.success) { + setGeneralError(result.error || "Password reset failed. Please try again."); } else { - console.log("Reset password successful with token: ", accessToken); - // Show confirmation toast + console.log("Password reset successful"); + setGeneralError(null); + // Show success message and redirect to login + alert("Password reset successfully! You can now login with your new password."); + window.location.href = '/auth/login'; } } catch (submissionError) { - setGeneralError("An error occurred during reset password process. Please try again."); + setGeneralError("An error occurred during password reset. Please try again."); + } finally { + setIsLoading(false); } - setTimeout(() => setIsLoading(false), 1000); // Simulate network delay - // setIsLoading(false); }; const handleChange = (e: React.ChangeEvent) => { @@ -152,7 +192,7 @@ export default function ResetPasswordPage() { diff --git a/frontend-nextjs/src/backend/core/user/UserService.ts b/frontend-nextjs/src/backend/core/user/UserService.ts index 0f75ef1..c632813 100644 --- a/frontend-nextjs/src/backend/core/user/UserService.ts +++ b/frontend-nextjs/src/backend/core/user/UserService.ts @@ -101,3 +101,75 @@ export async function loginUser(loginInput: LoginInput): Promise> { + try { + // Send password reset email using Supabase Auth + const { error } = await supabaseClient.auth.resetPasswordForEmail(email, { + redirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/reset-password`, + }); + + if (error) { + return { + success: false, + error: error.message || "Failed to send password reset email", + data: null + }; + } + + return { + success: true, + message: "Password reset email sent successfully", + data: null + }; + } catch (err: any) { + return { + success: false, + error: err.message || "Internal server error", + data: null + }; + } +} + +export async function resetPassword(accessToken: string, refreshToken: string, newPassword: string): Promise> { + try { + // First, verify the recovery token and establish a session + const { data: sessionData, error: sessionError } = await supabaseClient.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken, + }); + + if (sessionError || !sessionData.session) { + return { + success: false, + error: sessionError?.message || "Invalid or expired recovery token", + data: null + }; + } + + // Now update the user's password + const { error: updateError } = await supabaseClient.auth.updateUser({ + password: newPassword + }); + + if (updateError) { + return { + success: false, + error: updateError.message || "Failed to update password", + data: null + }; + } + + return { + success: true, + message: "Password reset successfully", + data: null + }; + } catch (err: any) { + return { + success: false, + error: err.message || "Internal server error", + data: null + }; + } +} +