Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions frontend-nextjs/src/app/api/auth/forgot-password/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
);
}
}
42 changes: 42 additions & 0 deletions frontend-nextjs/src/app/api/auth/reset-password/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
);
}
}
27 changes: 19 additions & 8 deletions frontend-nextjs/src/app/auth/forgot-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};


Expand Down
68 changes: 54 additions & 14 deletions frontend-nextjs/src/app/auth/reset-password/page.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -21,9 +20,35 @@ export default function ResetPasswordPage() {
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
const [generalError, setGeneralError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState<string | null>(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 (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-blue-50 via-white to-blue-50 px-4 py-12">
<div className="w-full max-w-md">
<div className="rounded-2xl bg-white p-8 shadow-xl shadow-gray-200/50">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Loading...</h2>
<p className="text-gray-600">Please wait while we verify your reset link.</p>
</div>
</div>
</div>
);
}

if (!accessToken) {
return (
Expand Down Expand Up @@ -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<HTMLInputElement | HTMLTextAreaElement>) => {
Expand Down Expand Up @@ -152,7 +192,7 @@ export default function ResetPasswordPage() {

<AuthSubmitButton
type="submit"
label="Send Reset Link"
label="Reset Password"
isLoading={isLoading}
/>
</form>
Expand Down
72 changes: 72 additions & 0 deletions frontend-nextjs/src/backend/core/user/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,75 @@ export async function loginUser(loginInput: LoginInput): Promise<ServiceResponse
}
}

export async function forgotPassword(email: string): Promise<ServiceResponse<null>> {
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<ServiceResponse<null>> {
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
};
}
}