From 965db40febf70442a8ab009485569080d8814cc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 15:26:54 +0000 Subject: [PATCH 1/4] Initial plan From 6ead48f397e6bd44247cb988f86a8082403f3518 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 15:34:40 +0000 Subject: [PATCH 2/4] Implement password reset flow with recovery token authentication Co-authored-by: jarifulovi <139060427+jarifulovi@users.noreply.github.com> --- .../src/app/api/auth/forgot-password/route.ts | 34 +++++++++ .../src/app/api/auth/reset-password/route.ts | 38 ++++++++++ .../src/app/auth/forgot-password/page.tsx | 27 ++++--- .../src/app/auth/reset-password/page.tsx | 34 ++++++--- .../src/backend/core/user/UserService.ts | 72 +++++++++++++++++++ 5 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 frontend-nextjs/src/app/api/auth/forgot-password/route.ts create mode 100644 frontend-nextjs/src/app/api/auth/reset-password/route.ts 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..0cd5833 --- /dev/null +++ b/frontend-nextjs/src/app/api/auth/forgot-password/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from 'next/server' +import { isValidEmail } from '@/backend/core/shared/validation' +import { forgotPassword } from '@/backend/core/user/UserService'; + + +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..ee77c69 --- /dev/null +++ b/frontend-nextjs/src/app/api/auth/reset-password/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server' +import { isValidPassword } from '@/backend/core/shared/validation' +import { resetPassword } from '@/backend/core/user/UserService'; + + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { accessToken, 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, 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..dd4b86b 100644 --- a/frontend-nextjs/src/app/auth/reset-password/page.tsx +++ b/frontend-nextjs/src/app/auth/reset-password/page.tsx @@ -75,22 +75,36 @@ 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, + 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 +166,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..293bd97 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, 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: '', // Not needed for recovery tokens + }); + + 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 + }; + } +} + From 3996c70196fd1b52d6927bbe32a1f30de3354b88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 15:36:44 +0000 Subject: [PATCH 3/4] Add documentation comments to API routes Co-authored-by: jarifulovi <139060427+jarifulovi@users.noreply.github.com> --- frontend-nextjs/src/app/api/auth/forgot-password/route.ts | 5 ++++- frontend-nextjs/src/app/api/auth/reset-password/route.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend-nextjs/src/app/api/auth/forgot-password/route.ts b/frontend-nextjs/src/app/api/auth/forgot-password/route.ts index 0cd5833..4b804d4 100644 --- a/frontend-nextjs/src/app/api/auth/forgot-password/route.ts +++ b/frontend-nextjs/src/app/api/auth/forgot-password/route.ts @@ -2,7 +2,10 @@ 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(); diff --git a/frontend-nextjs/src/app/api/auth/reset-password/route.ts b/frontend-nextjs/src/app/api/auth/reset-password/route.ts index ee77c69..e6a0edb 100644 --- a/frontend-nextjs/src/app/api/auth/reset-password/route.ts +++ b/frontend-nextjs/src/app/api/auth/reset-password/route.ts @@ -2,7 +2,11 @@ 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(); From 0639a94f3b3dacc9571659cc8a427f58e2cec71d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 15:38:55 +0000 Subject: [PATCH 4/4] Fix token extraction from URL hash and handle refresh token properly Co-authored-by: jarifulovi <139060427+jarifulovi@users.noreply.github.com> --- .../src/app/api/auth/reset-password/route.ts | 4 +-- .../src/app/auth/reset-password/page.tsx | 34 ++++++++++++++++--- .../src/backend/core/user/UserService.ts | 4 +-- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/frontend-nextjs/src/app/api/auth/reset-password/route.ts b/frontend-nextjs/src/app/api/auth/reset-password/route.ts index e6a0edb..a82dc19 100644 --- a/frontend-nextjs/src/app/api/auth/reset-password/route.ts +++ b/frontend-nextjs/src/app/api/auth/reset-password/route.ts @@ -10,7 +10,7 @@ import { resetPassword } from '@/backend/core/user/UserService'; export async function POST(request: NextRequest) { try { const body = await request.json(); - const { accessToken, newPassword } = body; + const { accessToken, refreshToken, newPassword } = body; // Validate required fields if (!accessToken) { @@ -21,7 +21,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Password must be at least 8 characters and include a number' }, { status: 400 }); } - const result = await resetPassword(accessToken, newPassword); + const result = await resetPassword(accessToken, refreshToken || '', newPassword); if (!result.success) { return NextResponse.json({ error: result.error }, { status: 500 }); diff --git a/frontend-nextjs/src/app/auth/reset-password/page.tsx b/frontend-nextjs/src/app/auth/reset-password/page.tsx index dd4b86b..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 ( @@ -84,6 +109,7 @@ export default function ResetPasswordPage() { }, body: JSON.stringify({ accessToken: accessToken, + refreshToken: refreshToken, newPassword: formData.password }), }); diff --git a/frontend-nextjs/src/backend/core/user/UserService.ts b/frontend-nextjs/src/backend/core/user/UserService.ts index 293bd97..c632813 100644 --- a/frontend-nextjs/src/backend/core/user/UserService.ts +++ b/frontend-nextjs/src/backend/core/user/UserService.ts @@ -130,12 +130,12 @@ export async function forgotPassword(email: string): Promise> { +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: '', // Not needed for recovery tokens + refresh_token: refreshToken, }); if (sessionError || !sessionData.session) {