From cc11f403de6fde0c91cff8ba19cc6e6a7eccd55e Mon Sep 17 00:00:00 2001 From: Yashwin Date: Wed, 28 Jan 2026 17:41:39 +0530 Subject: [PATCH] Update environment configuration for production, remove automated Jenkins pipeline, and refactor routing in App component to utilize AppRoutes. Enhance Login and ProtectedRoute components to manage user roles and navigation based on authentication status. --- .env | 2 + Jenkinsfile.automated => Jenkinsfile | 0 src/App.tsx | 95 +------ src/pages/CreateTenantWizard.tsx | 2 +- src/pages/Login.tsx | 35 ++- src/pages/ProtectedRoute.tsx | 27 +- src/pages/TenantLogin.tsx | 361 +++++++++++++++++++++++++++ src/pages/TenantProtectedRoute.tsx | 38 +++ src/routes/index.tsx | 48 ++++ src/routes/public-routes.tsx | 38 +++ src/routes/super-admin-routes.tsx | 50 ++++ src/routes/tenant-admin-routes.tsx | 21 ++ 12 files changed, 616 insertions(+), 101 deletions(-) rename Jenkinsfile.automated => Jenkinsfile (100%) create mode 100644 src/pages/TenantLogin.tsx create mode 100644 src/pages/TenantProtectedRoute.tsx create mode 100644 src/routes/index.tsx create mode 100644 src/routes/public-routes.tsx create mode 100644 src/routes/super-admin-routes.tsx create mode 100644 src/routes/tenant-admin-routes.tsx diff --git a/.env b/.env index 75d31ae..aef7f2b 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ +# VITE_FRONTEND_BASE_URL=http://localhost:5173 # VITE_API_BASE_URL=http://localhost:3000/api/v1 +VITE_FRONTEND_BASE_URL=https://qasure.tech4bizsolutions.com VITE_API_BASE_URL=https://backendqaasure.tech4bizsolutions.com/api/v1 diff --git a/Jenkinsfile.automated b/Jenkinsfile similarity index 100% rename from Jenkinsfile.automated rename to Jenkinsfile diff --git a/src/App.tsx b/src/App.tsx index 842b794..8bf63cc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,101 +1,12 @@ -import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { BrowserRouter } from "react-router-dom"; import { Toaster } from "sonner"; -import Login from "./pages/Login"; -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 NotFound from "./pages/NotFound"; -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"; +import { AppRoutes } from "@/routes"; function App() { return ( - - } /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - {/* Catch-all route for 404 */} - - - - } - /> - + ); } diff --git a/src/pages/CreateTenantWizard.tsx b/src/pages/CreateTenantWizard.tsx index 5428231..606c46d 100644 --- a/src/pages/CreateTenantWizard.tsx +++ b/src/pages/CreateTenantWizard.tsx @@ -102,7 +102,7 @@ const subscriptionTierOptions = [ // Helper function to get base URL without protocol const getBaseUrlWithoutProtocol = (): string => { - const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5173'; + const apiBaseUrl = import.meta.env.VITE_FRONTEND_BASE_URL || 'http://localhost:5173'; // Remove protocol (http:// or https://) return apiBaseUrl.replace(/^https?:\/\//, ''); }; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index fd2a4d9..668a914 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -29,7 +29,7 @@ type LoginFormData = z.infer; const Login = (): ReactElement => { const navigate = useNavigate(); const dispatch = useAppDispatch(); - const { isLoading, error, isAuthenticated } = useAppSelector((state) => state.auth); + const { isLoading, error, isAuthenticated, roles } = useAppSelector((state) => state.auth); const { register, @@ -47,9 +47,26 @@ const Login = (): ReactElement => { // Redirect if already authenticated useEffect(() => { if (isAuthenticated) { - navigate('/dashboard'); + // 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, navigate]); + }, [isAuthenticated, roles, navigate]); // Clear errors only on component mount, not on every auth state change useEffect(() => { @@ -72,8 +89,14 @@ const Login = (): ReactElement => { const message = result.message || 'Login successful'; const description = result.message ? undefined : 'Welcome back!'; showToast.success(message, description); - // Only navigate on success - navigate('/dashboard'); + + // Check roles after login to redirect appropriately + const userRoles = result.data.roles || []; + if (userRoles.includes('super_admin')) { + navigate('/dashboard'); + } else { + navigate('/tenant/dashboard'); + } } } catch (error: any) { // Clear Redux error state since we're handling errors locally @@ -140,7 +163,7 @@ const Login = (): ReactElement => {

- 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) => ( +
+
+

{stat.label}

+
+
+

{stat.value}

+
+
+ ))} +
+
+ + {/* 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}

+
+ )} + {!generalError && error && !errors.email && !errors.password && ( +
+

{error}

+
+ )} + +
{ + e.preventDefault(); + e.stopPropagation(); + handleSubmit(onSubmit)(e); + }} + className="space-y-4" + noValidate + > + {/* Email Field */} + + + {/* Password Field */} + + + {/* Remember Me & Forgot Password */} +
+
+ setRememberMe(e.target.checked)} + className="w-[18px] h-[18px] rounded border-[#d1d5db] bg-[#112868] text-[#112868] focus:ring-2 focus:ring-[#112868]" + /> + +
+ { + e.preventDefault(); + navigate('/tenant/forgot-password'); + }} + className="text-[13px] font-medium text-[#112868] underline" + > + Forgot Password? + +
+ + {/* Submit Button */} +
+ + {isLoading ? 'Signing in...' : 'Sign In'} + +
+ + {/* New to tenant */} + + + {/* SSO Section */} +
+
+

Single sign-on

+
+ {['Google', 'Microsoft', 'Okta'].map((provider) => ( + + ))} +
+
+
+ +
+ + {/* 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: , + // }, +];