- Welcome Back 1
+ Welcome Back
Sign in to your account to continue
diff --git a/src/pages/ProtectedRoute.tsx b/src/pages/ProtectedRoute.tsx
index 3a2f009..d19c6c5 100644
--- a/src/pages/ProtectedRoute.tsx
+++ b/src/pages/ProtectedRoute.tsx
@@ -7,9 +7,32 @@ interface ProtectedRouteProps {
}
const ProtectedRoute = ({ children }: ProtectedRouteProps): ReactElement => {
- const { isAuthenticated } = useAppSelector((state) => state.auth);
+ const { isAuthenticated, roles } = useAppSelector((state) => state.auth);
- return isAuthenticated ? <>{children}> : ;
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ // Check if user has super_admin role
+ // Handle both array and JSON string formats
+ let rolesArray: string[] = [];
+ if (Array.isArray(roles)) {
+ rolesArray = roles;
+ } else if (typeof roles === 'string') {
+ try {
+ rolesArray = JSON.parse(roles);
+ } catch {
+ rolesArray = [];
+ }
+ }
+ const hasSuperAdminRole = rolesArray && rolesArray.length > 0 && rolesArray.includes('super_admin');
+
+ if (!hasSuperAdminRole) {
+ // If not super_admin, redirect to tenant login
+ return ;
+ }
+
+ return <>{children}>;
};
export default ProtectedRoute;
diff --git a/src/pages/TenantLogin.tsx b/src/pages/TenantLogin.tsx
new file mode 100644
index 0000000..e4d67b8
--- /dev/null
+++ b/src/pages/TenantLogin.tsx
@@ -0,0 +1,361 @@
+import { useState, useEffect } from 'react';
+import { useNavigate } 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 } from 'lucide-react';
+import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks';
+import { loginAsync, clearError } from '@/store/authSlice';
+import { FormField } from '@/components/shared';
+import { PrimaryButton } from '@/components/shared';
+import type { LoginError } from '@/services/auth-service';
+import { showToast } from '@/utils/toast';
+
+// Zod validation schema
+const loginSchema = z.object({
+ email: z
+ .string()
+ .min(1, 'Email is required')
+ .email('Please enter a valid email address'),
+ password: z
+ .string()
+ .min(1, 'Password is required')
+ .min(6, 'Password must be at least 6 characters'),
+});
+
+type LoginFormData = z.infer;
+
+const TenantLogin = (): ReactElement => {
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const { isLoading, error, isAuthenticated, roles } = useAppSelector((state) => state.auth);
+
+ const {
+ register,
+ handleSubmit,
+ setError,
+ formState: { errors },
+ clearErrors,
+ } = useForm({
+ resolver: zodResolver(loginSchema),
+ mode: 'onBlur',
+ });
+
+ const [generalError, setGeneralError] = useState('');
+ const [rememberMe, setRememberMe] = useState(false);
+
+ // Redirect if already authenticated
+ useEffect(() => {
+ if (isAuthenticated) {
+ // Check if user is super_admin, redirect to super admin dashboard
+ // Handle both array and JSON string formats
+ let rolesArray: string[] = [];
+ if (Array.isArray(roles)) {
+ rolesArray = roles;
+ } else if (typeof roles === 'string') {
+ try {
+ rolesArray = JSON.parse(roles);
+ } catch {
+ rolesArray = [];
+ }
+ }
+ if (rolesArray.includes('super_admin')) {
+ navigate('/dashboard');
+ } else {
+ // Tenant admin - redirect to tenant dashboard
+ navigate('/tenant/dashboard');
+ }
+ }
+ }, [isAuthenticated, roles, navigate]);
+
+ // Clear errors on component mount
+ useEffect(() => {
+ dispatch(clearError());
+ setGeneralError('');
+ clearErrors();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const onSubmit = async (data: LoginFormData): Promise => {
+ setGeneralError('');
+ clearErrors();
+ dispatch(clearError());
+
+ try {
+ const result = await dispatch(loginAsync(data)).unwrap();
+ if (result) {
+ const message = result.message || 'Login successful';
+ showToast.success(message);
+
+ // Check roles after login to redirect appropriately
+ // Handle both array and JSON string formats
+ const userRoles = result.data.roles || [];
+ let rolesArray: string[] = [];
+ if (Array.isArray(userRoles)) {
+ rolesArray = userRoles;
+ } else if (typeof userRoles === 'string') {
+ try {
+ rolesArray = JSON.parse(userRoles);
+ } catch {
+ rolesArray = [];
+ }
+ }
+ if (rolesArray.includes('super_admin')) {
+ navigate('/dashboard');
+ } else {
+ navigate('/tenant/dashboard');
+ }
+ }
+ } catch (error: any) {
+ dispatch(clearError());
+ const loginError = error as LoginError;
+
+ if (loginError && typeof loginError === 'object') {
+ if ('details' in loginError && Array.isArray(loginError.details)) {
+ loginError.details.forEach((detail) => {
+ if (detail.path === 'email' || detail.path === 'password') {
+ setError(detail.path as keyof LoginFormData, {
+ type: 'server',
+ message: detail.message,
+ });
+ } else {
+ setGeneralError(detail.message);
+ }
+ });
+ } else if ('error' in loginError) {
+ if (typeof loginError.error === 'object' && loginError.error !== null && 'message' in loginError.error) {
+ setGeneralError(loginError.error.message || 'Login failed');
+ } else if (typeof loginError.error === 'string') {
+ setGeneralError(loginError.error);
+ } else {
+ setGeneralError('Login failed. Please check your credentials.');
+ }
+ } else {
+ setGeneralError('An unexpected error occurred. Please try again.');
+ }
+ } else if (error?.message) {
+ setGeneralError(error.message);
+ } else {
+ setGeneralError('Login failed. Please check your credentials and try again.');
+ }
+ }
+ };
+
+ return (
+
+ {/* Left Side - Blue Background */}
+
+
+ {/* Logo Section */}
+
+
+
+
+
+
+ QAssure
+
+
-
+
Tenant
+
+
+ {/* Heading */}
+
+
+ Secure access for
every tenant.
+
+
+
+ {/* Description */}
+
+
+ Log in to manage projects, approvals, documents, and more from a single, compliant control center.
+
+
+
+
+ {/* Stats Grid */}
+
+ {[
+ { label: 'Avg. tenant uptime', value: '99.98%' },
+ { label: 'Regions supported', value: '12+' },
+ { label: 'Active tenants', value: '480+' },
+ { label: 'Compliance checks', value: '24/7' },
+ ].map((stat, index) => (
+
+ ))}
+
+
+
+ {/* Footer Text */}
+
+
SSO, MFA, and audit-ready access for every tenant.
+
Need help? Contact your workspace administrator.
+
+
+
+ {/* Right Side - Login Form */}
+
+
+ {/* Header */}
+
+
+
+ Tenant Admin Portal
+
+
+
Sign in to your tenant
+
+ Use your work email or configured SSO provider to access the admin portal.
+
+
+
+ {/* Login Card */}
+
+ {/* General Error Message */}
+ {generalError && (
+
+ )}
+ {!generalError && error && !errors.email && !errors.password && (
+
+ )}
+
+
+
+
+ {/* Footer Links */}
+
+
+
+
+ );
+};
+
+export default TenantLogin;
diff --git a/src/pages/TenantProtectedRoute.tsx b/src/pages/TenantProtectedRoute.tsx
new file mode 100644
index 0000000..b6adb07
--- /dev/null
+++ b/src/pages/TenantProtectedRoute.tsx
@@ -0,0 +1,38 @@
+import { Navigate } from 'react-router-dom';
+import { useAppSelector } from '@/hooks/redux-hooks';
+import type { ReactElement } from 'react';
+
+interface TenantProtectedRouteProps {
+ children: React.ReactNode;
+}
+
+const TenantProtectedRoute = ({ children }: TenantProtectedRouteProps): ReactElement => {
+ const { isAuthenticated, roles } = useAppSelector((state) => state.auth);
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ // Check if user has super_admin role - if yes, redirect to super admin dashboard
+ // Handle both array and JSON string formats
+ let rolesArray: string[] = [];
+ if (Array.isArray(roles)) {
+ rolesArray = roles;
+ } else if (typeof roles === 'string') {
+ try {
+ rolesArray = JSON.parse(roles);
+ } catch {
+ rolesArray = [];
+ }
+ }
+ const hasSuperAdminRole = rolesArray.includes('super_admin');
+
+ if (hasSuperAdminRole) {
+ // If super_admin, redirect to super admin dashboard
+ return ;
+ }
+
+ return <>{children}>;
+};
+
+export default TenantProtectedRoute;
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
new file mode 100644
index 0000000..46ca584
--- /dev/null
+++ b/src/routes/index.tsx
@@ -0,0 +1,48 @@
+import { Routes, Route } from 'react-router-dom';
+import type { ReactElement } from 'react';
+import NotFound from '@/pages/NotFound';
+import ProtectedRoute from '@/pages/ProtectedRoute';
+import TenantProtectedRoute from '@/pages/TenantProtectedRoute';
+import { publicRoutes } from './public-routes';
+import { superAdminRoutes } from './super-admin-routes';
+import { tenantAdminRoutes } from './tenant-admin-routes';
+
+// App Routes Component
+export const AppRoutes = (): ReactElement => {
+ return (
+
+ {/* Public Routes */}
+ {publicRoutes.map((route) => (
+
+ ))}
+
+ {/* Super Admin Routes */}
+ {superAdminRoutes.map((route) => (
+ {route.element}}
+ />
+ ))}
+
+ {/* Tenant Admin Routes */}
+ {tenantAdminRoutes.map((route) => (
+ {route.element}}
+ />
+ ))}
+
+ {/* 404 - Catch all route */}
+
+
+
+ }
+ />
+
+ );
+};
diff --git a/src/routes/public-routes.tsx b/src/routes/public-routes.tsx
new file mode 100644
index 0000000..0ce3539
--- /dev/null
+++ b/src/routes/public-routes.tsx
@@ -0,0 +1,38 @@
+import Login from '@/pages/Login';
+import TenantLogin from '@/pages/TenantLogin';
+import ForgotPassword from '@/pages/ForgotPassword';
+import ResetPassword from '@/pages/ResetPassword';
+import type { ReactElement } from 'react';
+
+export interface RouteConfig {
+ path: string;
+ element: ReactElement;
+}
+
+// Public routes (no authentication required)
+export const publicRoutes: RouteConfig[] = [
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/forgot-password',
+ element: ,
+ },
+ {
+ path: '/reset-password',
+ element: ,
+ },
+ {
+ path: '/tenant/login',
+ element: ,
+ },
+ {
+ path: '/tenant/forgot-password',
+ element: ,
+ },
+ {
+ path: '/tenant/reset-password',
+ element: ,
+ },
+];
diff --git a/src/routes/super-admin-routes.tsx b/src/routes/super-admin-routes.tsx
new file mode 100644
index 0000000..6e2bbc1
--- /dev/null
+++ b/src/routes/super-admin-routes.tsx
@@ -0,0 +1,50 @@
+import Dashboard from '@/pages/Dashboard';
+import Tenants from '@/pages/Tenants';
+import CreateTenantWizard from '@/pages/CreateTenantWizard';
+import TenantDetails from '@/pages/TenantDetails';
+import Users from '@/pages/Users';
+import Roles from '@/pages/Roles';
+import Modules from '@/pages/Modules';
+import AuditLogs from '@/pages/AuditLogs';
+import type { ReactElement } from 'react';
+
+export interface RouteConfig {
+ path: string;
+ element: ReactElement;
+}
+
+// Super Admin routes (requires super_admin role)
+export const superAdminRoutes: RouteConfig[] = [
+ {
+ path: '/dashboard',
+ element: ,
+ },
+ {
+ path: '/tenants',
+ element: ,
+ },
+ {
+ path: '/tenants/create-wizard',
+ element: ,
+ },
+ {
+ path: '/tenants/:id',
+ element: ,
+ },
+ {
+ path: '/users',
+ element: ,
+ },
+ {
+ path: '/roles',
+ element: ,
+ },
+ {
+ path: '/modules',
+ element: ,
+ },
+ {
+ path: '/audit-logs',
+ element: ,
+ },
+];
diff --git a/src/routes/tenant-admin-routes.tsx b/src/routes/tenant-admin-routes.tsx
new file mode 100644
index 0000000..3d0a4e5
--- /dev/null
+++ b/src/routes/tenant-admin-routes.tsx
@@ -0,0 +1,21 @@
+import Dashboard from '@/pages/Dashboard';
+import type { ReactElement } from 'react';
+
+export interface RouteConfig {
+ path: string;
+ element: ReactElement;
+}
+
+// Tenant Admin routes (requires authentication but NOT super_admin role)
+export const tenantAdminRoutes: RouteConfig[] = [
+ {
+ path: '/tenant/dashboard',
+ element: , // TODO: Replace with TenantDashboard when created
+ },
+ // Add more tenant admin routes here as needed
+ // Example:
+ // {
+ // path: '/tenant/users',
+ // element: ,
+ // },
+];