From f4f1225f023002a788eca8175dff500ee254aa57 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Wed, 21 Jan 2026 11:20:16 +0530 Subject: [PATCH] Implement password recovery features by adding Forgot Password and Reset Password pages, update routing in App component, and enhance dashboard statistics fetching with error handling. Update .env for API base URL. --- .env | 2 +- src/App.tsx | 4 + src/components/shared/NewUserModal.tsx | 2 +- .../dashboard/components/StatsGrid.tsx | 141 ++++++++--- src/pages/Dashboard.tsx | 16 +- src/pages/ForgotPassword.tsx | 176 ++++++++++++++ src/pages/Login.tsx | 14 ++ src/pages/ResetPassword.tsx | 228 ++++++++++++++++++ src/services/auth-service.ts | 33 +++ src/services/dashboard-service.ts | 22 ++ 10 files changed, 589 insertions(+), 49 deletions(-) create mode 100644 src/pages/ForgotPassword.tsx create mode 100644 src/pages/ResetPassword.tsx create mode 100644 src/services/dashboard-service.ts diff --git a/.env b/.env index 0b220de..75d31ae 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ # VITE_API_BASE_URL=http://localhost:3000/api/v1 -VITE_API_BASE_URL=https://qasure.tech4bizsolutions.com/api/v1 +VITE_API_BASE_URL=https://backendqaasure.tech4bizsolutions.com/api/v1 diff --git a/src/App.tsx b/src/App.tsx index fe7a22f..16a6bf9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,12 +8,16 @@ import ProtectedRoute from "./pages/ProtectedRoute"; import Roles from "./pages/Roles"; import Modules from "./pages/Modules"; import AuditLogs from "./pages/AuditLogs"; +import ForgotPassword from "./pages/ForgotPassword"; +import ResetPassword from "./pages/ResetPassword"; function App() { return ( } /> + } /> + } /> { + const [statsData, setStatsData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchStatistics = async () => { + try { + setIsLoading(true); + setError(null); + const response = await dashboardService.getStatistics(); + const { data } = response; + + const mappedStats: StatCardData[] = [ + { + icon: Building2, + value: data.totalTenants, + label: 'Total Tenants', + badge: { text: `${data.activeTenants} active`, variant: 'green' }, + }, + { + icon: CheckCircle2, + value: data.activeTenants, + label: 'Active Tenants', + badge: { + text: data.totalTenants > 0 + ? `${Math.round((data.activeTenants / data.totalTenants) * 100)}% Rate` + : '0% Rate', + variant: 'green', + }, + }, + { + icon: Users, + value: data.totalUsers, + label: 'Total Users', + badge: { text: 'All users', variant: 'gray' }, + }, + { + icon: TrendingUp, + value: data.activeSessions, + label: 'Active Sessions', + badge: { text: 'Live now', variant: 'gray' }, + }, + { + icon: Package, + value: data.registeredModules, + label: 'Registered Modules', + badge: { text: 'Total', variant: 'gray' }, + }, + { + icon: Heart, + value: data.healthyModules, + label: 'Healthy Modules', + badge: { + text: data.registeredModules > 0 + ? `${Math.round((data.healthyModules / data.registeredModules) * 100)}% Uptime` + : '0% Uptime', + variant: data.healthyModules === data.registeredModules && data.registeredModules > 0 + ? 'green' + : 'gray', + }, + }, + ]; + + setStatsData(mappedStats); + } catch (err) { + console.error('Failed to fetch dashboard statistics:', err); + setError('Failed to load statistics. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + fetchStatistics(); + }, []); + + if (isLoading) { + return ( +
+ {Array.from({ length: 6 }).map((_, index) => ( +
+
+
+
+
+ ))} +
+ ); + } + + if (error) { + return ( +
+

{error}

+
+ ); + } + return (
{statsData.map((stat, index) => ( diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index d234f11..ed29824 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -14,17 +14,17 @@ const Dashboard = (): ReactElement => { description: 'Monitor system health, tenant activity, and user metrics in real-time.', }} > - {/* Stats Grid */} - + {/* Stats Grid */} + - {/* Bottom Section */} + {/* Bottom Section */}
- +
- - -
-
+ + +
+ ); }; diff --git a/src/pages/ForgotPassword.tsx b/src/pages/ForgotPassword.tsx new file mode 100644 index 0000000..4989a01 --- /dev/null +++ b/src/pages/ForgotPassword.tsx @@ -0,0 +1,176 @@ +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import type { ReactElement } from 'react'; +import { Shield, ArrowLeft } from 'lucide-react'; +import { authService } from '@/services/auth-service'; +import { FormField } from '@/components/shared'; +import { PrimaryButton } from '@/components/shared'; + +// Zod validation schema +const forgotPasswordSchema = z.object({ + email: z + .string() + .min(1, 'Email is required') + .email('Please enter a valid email address'), +}); + +type ForgotPasswordFormData = z.infer; + +const ForgotPassword = (): ReactElement => { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const { + register, + handleSubmit, + formState: { errors }, + clearErrors, + } = useForm({ + resolver: zodResolver(forgotPasswordSchema), + mode: 'onBlur', + }); + + const onSubmit = async (data: ForgotPasswordFormData): Promise => { + setError(''); + setSuccess(''); + clearErrors(); + setIsLoading(true); + + try { + const response = await authService.forgotPassword({ email: data.email }); + if (response.success && response.data) { + setSuccess(response.data.message || 'If an account exists with this email, a password reset link has been sent.'); + } + } catch (err: any) { + console.error('Forgot password error:', err); + if (err?.response?.data?.error?.message) { + setError(err.response.data.error.message); + } else if (err?.response?.data?.error) { + setError(typeof err.response.data.error === 'string' ? err.response.data.error : 'Failed to send reset email'); + } else if (err?.message) { + setError(err.message); + } else { + setError('Failed to send reset email. Please try again.'); + } + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {/* Forgot Password Card */} +
+ {/* Logo Section */} +
+
+
+ +
+
+ QAssure +
+
+
+
+

+ Forgot Password +

+

+ Enter your email address and we'll send you a link to reset your password +

+
+ + {/* Success Message */} + {success && ( +
+

{success}

+
+ )} + + {/* Error Message */} + {error && ( +
+

{error}

+
+ )} + + {!success ? ( +
{ + e.preventDefault(); + e.stopPropagation(); + handleSubmit(onSubmit)(e); + }} + className="space-y-4" + noValidate + > + {/* Email Field */} + + + {/* Submit Button */} +
+ + {isLoading ? 'Sending...' : 'Send Reset Link'} + +
+ + ) : ( +
+
+ navigate('/')} + className="w-full h-12 text-sm font-medium" + > + Back to Login + +
+
+ )} + + {/* Back to Login Link */} + {!success && ( +
+ + + Back to Login + +
+ )} +
+ + {/* Footer */} +
+

+ © 2026 QAssure. All rights reserved. +

+
+
+
+ ); +}; + +export default ForgotPassword; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 5945340..3824e82 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -185,6 +185,20 @@ const Login = (): ReactElement => { {...register('password')} /> + {/* Forgot Password Link */} + + {/* Submit Button */}
data.password === data.confirmPassword, { + message: 'Passwords do not match', + path: ['confirmPassword'], + }); + +type ResetPasswordFormData = z.infer; + +const ResetPassword = (): ReactElement => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + const tokenFromUrl = searchParams.get('token') || ''; + + const { + register, + handleSubmit, + setValue, + formState: { errors }, + clearErrors, + } = useForm({ + resolver: zodResolver(resetPasswordSchema), + mode: 'onBlur', + defaultValues: { + token: tokenFromUrl, + }, + }); + + // Set token from URL if available + useEffect(() => { + if (tokenFromUrl) { + setValue('token', tokenFromUrl); + } + }, [tokenFromUrl, setValue]); + + const onSubmit = async (data: ResetPasswordFormData): Promise => { + setError(''); + setSuccess(''); + clearErrors(); + setIsLoading(true); + + try { + const response = await authService.resetPassword({ + token: data.token, + password: data.password, + }); + if (response.success) { + setSuccess(response.data?.message || response.message || 'Password reset successfully! You can now login with your new password.'); + // Redirect to login after 2 seconds + setTimeout(() => { + navigate('/'); + }, 2000); + } + } catch (err: any) { + console.error('Reset password error:', err); + if (err?.response?.data?.error?.message) { + setError(err.response.data.error.message); + } else if (err?.response?.data?.error) { + setError(typeof err.response.data.error === 'string' ? err.response.data.error : 'Failed to reset password'); + } else if (err?.response?.data?.details && Array.isArray(err.response.data.details)) { + // Handle validation errors + const firstError = err.response.data.details[0]; + setError(firstError.message || 'Validation error'); + } else if (err?.message) { + setError(err.message); + } else { + setError('Failed to reset password. Please check your token and try again.'); + } + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {/* Reset Password Card */} +
+ {/* Logo Section */} +
+
+
+ +
+
+ QAssure +
+
+
+
+

+ Reset Password +

+

+ Enter your reset token and new password +

+
+ + {/* Success Message */} + {success && ( +
+

{success}

+
+ )} + + {/* Error Message */} + {error && ( +
+

{error}

+
+ )} + + {!success ? ( +
{ + e.preventDefault(); + e.stopPropagation(); + handleSubmit(onSubmit)(e); + }} + className="space-y-4" + noValidate + > + {/* Token Field */} + + + {/* Password Field */} + + + {/* Confirm Password Field */} + + + {/* Submit Button */} +
+ + {isLoading ? 'Resetting...' : 'Reset Password'} + +
+ + ) : ( +
+
+ navigate('/')} + className="w-full h-12 text-sm font-medium" + > + Go to Login + +
+
+ )} + + {/* Back to Login Link */} + {!success && ( +
+ + + Back to Login + +
+ )} +
+ + {/* Footer */} +
+

+ © 2026 QAssure. All rights reserved. +

+
+
+
+ ); +}; + +export default ResetPassword; diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts index d1ff676..ce75c35 100644 --- a/src/services/auth-service.ts +++ b/src/services/auth-service.ts @@ -51,6 +51,31 @@ export interface LogoutResponse { message?: string; } +export interface ForgotPasswordRequest { + email: string; +} + +export interface ForgotPasswordResponse { + success: boolean; + data: { + success: boolean; + message: string; + }; +} + +export interface ResetPasswordRequest { + token: string; + password: string; +} + +export interface ResetPasswordResponse { + success: boolean; + data?: { + message?: string; + }; + message?: string; +} + export const authService = { login: async (credentials: LoginRequest): Promise => { const response = await apiClient.post('/auth/login', credentials); @@ -61,4 +86,12 @@ export const authService = { const response = await apiClient.post('/auth/logout', {}); return response.data; }, + forgotPassword: async (data: ForgotPasswordRequest): Promise => { + const response = await apiClient.post('/auth/forgot-password', data); + return response.data; + }, + resetPassword: async (data: ResetPasswordRequest): Promise => { + const response = await apiClient.post('/auth/reset-password', data); + return response.data; + }, }; diff --git a/src/services/dashboard-service.ts b/src/services/dashboard-service.ts new file mode 100644 index 0000000..ee7508a --- /dev/null +++ b/src/services/dashboard-service.ts @@ -0,0 +1,22 @@ +import apiClient from './api-client'; + +export interface DashboardStatistics { + totalTenants: number; + activeTenants: number; + totalUsers: number; + activeSessions: number; + registeredModules: number; + healthyModules: number; +} + +export interface DashboardStatisticsResponse { + success: boolean; + data: DashboardStatistics; +} + +export const dashboardService = { + getStatistics: async (): Promise => { + const response = await apiClient.get('/dashboard/statistics'); + return response.data; + }, +};