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; + }, +};