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.
This commit is contained in:
parent
60b14f3fe7
commit
cc11f403de
2
.env
2
.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
|
||||
|
||||
0
Jenkinsfile.automated → Jenkinsfile
vendored
0
Jenkinsfile.automated → Jenkinsfile
vendored
95
src/App.tsx
95
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 (
|
||||
<BrowserRouter>
|
||||
<Toaster position="top-right" richColors />
|
||||
<Routes>
|
||||
<Route path="/" element={<Login />} />
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tenants"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Tenants />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tenants/create-wizard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<CreateTenantWizard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tenants/:id"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<TenantDetails />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/users"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Users />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/roles"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Roles />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/modules"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Modules />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/audit-logs"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AuditLogs />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* Catch-all route for 404 */}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<NotFound />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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?:\/\//, '');
|
||||
};
|
||||
|
||||
@ -29,7 +29,7 @@ type LoginFormData = z.infer<typeof loginSchema>;
|
||||
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 => {
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-[#0f1724] mb-2">
|
||||
Welcome Back 1
|
||||
Welcome Back
|
||||
</h1>
|
||||
<p className="text-sm md:text-base text-[#6b7280]">
|
||||
Sign in to your account to continue
|
||||
|
||||
@ -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}</> : <Navigate to="/" replace />;
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
// 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 <Navigate to="/tenant/login" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
|
||||
361
src/pages/TenantLogin.tsx
Normal file
361
src/pages/TenantLogin.tsx
Normal file
@ -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<typeof loginSchema>;
|
||||
|
||||
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<LoginFormData>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
mode: 'onBlur',
|
||||
});
|
||||
|
||||
const [generalError, setGeneralError] = useState<string>('');
|
||||
const [rememberMe, setRememberMe] = useState<boolean>(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<void> => {
|
||||
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 (
|
||||
<div className="min-h-screen bg-[#f6f9ff] relative flex">
|
||||
{/* Left Side - Blue Background */}
|
||||
<div className="hidden lg:flex lg:w-[48%] bg-[#112868] flex-col justify-between px-8 py-8 min-w-[320px]">
|
||||
<div className="flex flex-col gap-2.5 py-22">
|
||||
{/* Logo Section */}
|
||||
<div className="flex flex-col gap-3 max-w-[280px]">
|
||||
<div className="flex items-center justify-between px-2 w-[206px]">
|
||||
<div className="bg-[#23dce1] rounded-[10px] shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)] w-9 h-9 flex items-center justify-center shrink-0">
|
||||
<Shield className="w-5 h-5 text-white" strokeWidth={1.67} />
|
||||
</div>
|
||||
<div className="text-[18px] font-bold text-[#23dce1] tracking-[-0.36px]">
|
||||
QAssure
|
||||
</div>
|
||||
<div className="text-[13px] font-medium text-white uppercase">-</div>
|
||||
<div className="text-[13px] font-medium text-white uppercase">Tenant</div>
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-2xl font-semibold text-white leading-normal">
|
||||
Secure access for<br />every tenant.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="opacity-90">
|
||||
<p className="text-sm font-medium text-white leading-[21px]">
|
||||
Log in to manage projects, approvals, documents, and more from a single, compliant control center.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-2.5 mt-16 max-w-[377px]">
|
||||
{[
|
||||
{ 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) => (
|
||||
<div
|
||||
key={index}
|
||||
className="backdrop-blur-[20px] border border-white rounded-md p-3"
|
||||
style={{
|
||||
backgroundImage: 'linear-gradient(108.34deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.3) 100.2%)',
|
||||
}}
|
||||
>
|
||||
<div className="opacity-90 mb-1">
|
||||
<p className="text-xs font-normal text-white leading-normal">{stat.label}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-base font-semibold text-white leading-normal">{stat.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Text */}
|
||||
<div className="flex flex-col gap-1 opacity-90">
|
||||
<p className="text-sm font-normal text-white">SSO, MFA, and audit-ready access for every tenant.</p>
|
||||
<p className="text-sm font-normal text-white">Need help? Contact your workspace administrator.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Login Form */}
|
||||
<div className="flex-1 flex items-center justify-center px-9 py-8">
|
||||
<div className="w-full max-w-[507px] flex flex-col gap-5">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="bg-[#edf3fe] px-2.5 py-1 rounded-full">
|
||||
<span className="text-[11px] font-medium text-[#0f1724]">Tenant Admin Portal</span>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-[22px] font-semibold text-[#0f1724]">Sign in to your tenant</h1>
|
||||
<p className="text-sm font-normal text-[#6b7280]">
|
||||
Use your work email or configured SSO provider to access the admin portal.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Login Card */}
|
||||
<div className="bg-white border border-[#d1d5db] rounded-lg p-5 flex flex-col gap-2.5">
|
||||
{/* General Error Message */}
|
||||
{generalError && (
|
||||
<div className="mb-4 p-3 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
|
||||
<p className="text-sm text-[#ef4444]">{generalError}</p>
|
||||
</div>
|
||||
)}
|
||||
{!generalError && error && !errors.email && !errors.password && (
|
||||
<div className="mb-4 p-3 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md">
|
||||
<p className="text-sm text-[#ef4444]">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
className="space-y-4"
|
||||
noValidate
|
||||
>
|
||||
{/* Email Field */}
|
||||
<FormField
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
error={errors.email?.message}
|
||||
{...register('email')}
|
||||
/>
|
||||
|
||||
{/* Password Field */}
|
||||
<FormField
|
||||
label="Password"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
error={errors.password?.message}
|
||||
{...register('password')}
|
||||
/>
|
||||
|
||||
{/* Remember Me & Forgot Password */}
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="remember-me"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
className="w-[18px] h-[18px] rounded border-[#d1d5db] bg-[#112868] text-[#112868] focus:ring-2 focus:ring-[#112868]"
|
||||
/>
|
||||
<label htmlFor="remember-me" className="text-sm font-normal text-[#0f1724] cursor-pointer">
|
||||
Remember Me
|
||||
</label>
|
||||
</div>
|
||||
<a
|
||||
href="/tenant/forgot-password"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate('/tenant/forgot-password');
|
||||
}}
|
||||
className="text-[13px] font-medium text-[#112868] underline"
|
||||
>
|
||||
Forgot Password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="pt-1">
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
size="large"
|
||||
disabled={isLoading}
|
||||
className="w-full h-12 text-base font-medium"
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign In'}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
|
||||
{/* New to tenant */}
|
||||
<div className="flex items-center justify-between h-8">
|
||||
<p className="text-xs font-normal text-[#6b7280]">New to this tenant?</p>
|
||||
<a
|
||||
href="/tenant/request-access"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate('/tenant/request-access');
|
||||
}}
|
||||
className="text-[13px] font-medium text-[#23dce1] underline"
|
||||
>
|
||||
Request Access
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* SSO Section */}
|
||||
<div className="pt-1">
|
||||
<div className="flex flex-col gap-2.5 h-[60px]">
|
||||
<p className="text-xs font-normal text-[#6b7280] uppercase tracking-[0.48px]">Single sign-on</p>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{['Google', 'Microsoft', 'Okta'].map((provider) => (
|
||||
<button
|
||||
key={provider}
|
||||
type="button"
|
||||
className="bg-white border border-[#9ca3af] rounded px-4 py-1 flex items-center justify-center gap-2 h-10 text-sm font-medium text-[#0f1724] hover:bg-[#f5f7fa] transition-colors"
|
||||
>
|
||||
{provider}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer Links */}
|
||||
<div className="flex gap-4 items-start">
|
||||
<a href="/tenant/status" className="text-xs font-normal text-[#6b7280]">
|
||||
Status page
|
||||
</a>
|
||||
<a href="/tenant/security" className="text-xs font-normal text-[#6b7280]">
|
||||
Security
|
||||
</a>
|
||||
<a href="/tenant/help" className="text-xs font-normal text-[#6b7280]">
|
||||
Help
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TenantLogin;
|
||||
38
src/pages/TenantProtectedRoute.tsx
Normal file
38
src/pages/TenantProtectedRoute.tsx
Normal file
@ -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 <Navigate to="/tenant/login" replace />;
|
||||
}
|
||||
|
||||
// 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 <Navigate to="/dashboard" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default TenantProtectedRoute;
|
||||
48
src/routes/index.tsx
Normal file
48
src/routes/index.tsx
Normal file
@ -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 (
|
||||
<Routes>
|
||||
{/* Public Routes */}
|
||||
{publicRoutes.map((route) => (
|
||||
<Route key={route.path} path={route.path} element={route.element} />
|
||||
))}
|
||||
|
||||
{/* Super Admin Routes */}
|
||||
{superAdminRoutes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={<ProtectedRoute>{route.element}</ProtectedRoute>}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Tenant Admin Routes */}
|
||||
{tenantAdminRoutes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={<TenantProtectedRoute>{route.element}</TenantProtectedRoute>}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 404 - Catch all route */}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<NotFound />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
38
src/routes/public-routes.tsx
Normal file
38
src/routes/public-routes.tsx
Normal file
@ -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: <Login />,
|
||||
},
|
||||
{
|
||||
path: '/forgot-password',
|
||||
element: <ForgotPassword />,
|
||||
},
|
||||
{
|
||||
path: '/reset-password',
|
||||
element: <ResetPassword />,
|
||||
},
|
||||
{
|
||||
path: '/tenant/login',
|
||||
element: <TenantLogin />,
|
||||
},
|
||||
{
|
||||
path: '/tenant/forgot-password',
|
||||
element: <ForgotPassword />,
|
||||
},
|
||||
{
|
||||
path: '/tenant/reset-password',
|
||||
element: <ResetPassword />,
|
||||
},
|
||||
];
|
||||
50
src/routes/super-admin-routes.tsx
Normal file
50
src/routes/super-admin-routes.tsx
Normal file
@ -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: <Dashboard />,
|
||||
},
|
||||
{
|
||||
path: '/tenants',
|
||||
element: <Tenants />,
|
||||
},
|
||||
{
|
||||
path: '/tenants/create-wizard',
|
||||
element: <CreateTenantWizard />,
|
||||
},
|
||||
{
|
||||
path: '/tenants/:id',
|
||||
element: <TenantDetails />,
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
element: <Users />,
|
||||
},
|
||||
{
|
||||
path: '/roles',
|
||||
element: <Roles />,
|
||||
},
|
||||
{
|
||||
path: '/modules',
|
||||
element: <Modules />,
|
||||
},
|
||||
{
|
||||
path: '/audit-logs',
|
||||
element: <AuditLogs />,
|
||||
},
|
||||
];
|
||||
21
src/routes/tenant-admin-routes.tsx
Normal file
21
src/routes/tenant-admin-routes.tsx
Normal file
@ -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: <Dashboard />, // TODO: Replace with TenantDashboard when created
|
||||
},
|
||||
// Add more tenant admin routes here as needed
|
||||
// Example:
|
||||
// {
|
||||
// path: '/tenant/users',
|
||||
// element: <TenantUsers />,
|
||||
// },
|
||||
];
|
||||
Loading…
Reference in New Issue
Block a user