Compare commits
2 Commits
fd29e867f0
...
91fe238547
| Author | SHA1 | Date | |
|---|---|---|---|
| 91fe238547 | |||
| 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_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
|
VITE_API_BASE_URL=https://backendqaasure.tech4bizsolutions.com/api/v1
|
||||||
|
|||||||
378
Jenkinsfile
vendored
378
Jenkinsfile
vendored
@ -1,221 +1,263 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent any
|
agent any
|
||||||
|
|
||||||
environment {
|
|
||||||
SSH_CREDENTIALS = 'ltts'
|
|
||||||
REMOTE_SERVER = 'ubuntu@160.187.166.95'
|
|
||||||
REMOTE_WORKSPACE = '/home/ubuntu'
|
|
||||||
PROJECT_NAME = 'qassure-frontend'
|
|
||||||
DEPLOY_PATH = '/var/www/qassure-ui'
|
|
||||||
GIT_CREDENTIALS = 'git-cred'
|
|
||||||
REPO_URL = 'https://git.tech4biz.wiki/yashwin/Qassure-frontend.git'
|
|
||||||
NPM_PATH = '/home/ubuntu/.nvm/versions/node/v22.17.0/bin/npm'
|
|
||||||
NODE_PATH = '/home/ubuntu/.nvm/versions/node/v22.17.0/bin/node'
|
|
||||||
EMAIL_RECIPIENT = 'sibarchan.nayak@tech4biz.org'
|
|
||||||
}
|
|
||||||
|
|
||||||
options {
|
options {
|
||||||
timeout(time: 30, unit: 'MINUTES')
|
disableConcurrentBuilds()
|
||||||
retry(2)
|
|
||||||
timestamps()
|
timestamps()
|
||||||
|
timeout(time: 30, unit: 'MINUTES')
|
||||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
// --- Deployment Target ---
|
||||||
|
SERVER_IP = '160.187.166.95'
|
||||||
|
SERVER_USER = 'ubuntu'
|
||||||
|
PROJECT_DIR = '/home/ubuntu/ltts/Qassure-frontend'
|
||||||
|
|
||||||
|
// --- Git Config ---
|
||||||
|
// Using the verified repository URL
|
||||||
|
GIT_REPO = 'https://git.tech4biz.wiki/yashwin/Qassure-frontend.git'
|
||||||
|
GIT_BRANCH = 'main'
|
||||||
|
GIT_CREDENTIALS_ID = 'git-cred'
|
||||||
|
|
||||||
|
// --- SSH Config ---
|
||||||
|
SSH_KEY_CREDENTIALS_ID = 'ltts'
|
||||||
|
|
||||||
|
// --- Application Config ---
|
||||||
|
NODE_HOME = '/home/ubuntu/.nvm/versions/node/v22.17.0'
|
||||||
|
|
||||||
|
// --- Build Config ---
|
||||||
|
VITE_API_BASE_URL = 'https://backendqaasure.tech4bizsolutions.com/api/v1'
|
||||||
|
|
||||||
|
// --- System Config ---
|
||||||
|
BACKUP_DIR = '/home/ubuntu/backups/qassure-frontend'
|
||||||
|
EMAIL_RECIPIENTS = 'mohammed.yaseen@tech4biz.org'
|
||||||
|
|
||||||
|
DEPLOY_START_TIME = "${System.currentTimeMillis()}"
|
||||||
|
}
|
||||||
|
|
||||||
stages {
|
stages {
|
||||||
stage('Preparation') {
|
stage('Initialize') {
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
echo "Starting ${PROJECT_NAME} deployment pipeline"
|
echo "🚀 Starting QAssur Frontend Deployment Pipeline"
|
||||||
echo "Server: ${REMOTE_SERVER}"
|
echo "📍 Target: ${SERVER_IP}"
|
||||||
echo "Deploy Path: ${DEPLOY_PATH}"
|
echo "🌿 Branch: ${GIT_BRANCH}"
|
||||||
|
echo "📂 Directory: ${PROJECT_DIR}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Git Operations on Remote Server') {
|
stage('Setup Project Directory') {
|
||||||
steps {
|
steps {
|
||||||
|
sshagent(credentials: [SSH_KEY_CREDENTIALS_ID]) {
|
||||||
script {
|
script {
|
||||||
sshagent(credentials: [SSH_CREDENTIALS]) {
|
withCredentials([usernamePassword(
|
||||||
withCredentials([usernamePassword(credentialsId: GIT_CREDENTIALS, usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASS')]) {
|
credentialsId: GIT_CREDENTIALS_ID,
|
||||||
|
usernameVariable: 'GIT_USERNAME',
|
||||||
|
passwordVariable: 'GIT_PASSWORD'
|
||||||
|
)]) {
|
||||||
sh """
|
sh """
|
||||||
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
|
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${SERVER_USER}@${SERVER_IP} '
|
||||||
set -e
|
set -e
|
||||||
|
echo "📁 Setting up project directory..."
|
||||||
|
|
||||||
echo "Checking Git repo..."
|
mkdir -p "\$(dirname "${PROJECT_DIR}")"
|
||||||
if [ -d "${DEPLOY_PATH}/.git" ]; then
|
mkdir -p ${BACKUP_DIR}
|
||||||
echo "Pulling latest code..."
|
|
||||||
cd ${DEPLOY_PATH}
|
|
||||||
|
|
||||||
# Fix ownership issues
|
if [ ! -d "${PROJECT_DIR}/.git" ]; then
|
||||||
sudo chown -R ubuntu:ubuntu ${DEPLOY_PATH}
|
echo "📥 Repository not found. Cloning from scratch..."
|
||||||
git config --global --add safe.directory ${DEPLOY_PATH}
|
cd "\$(dirname "${PROJECT_DIR}")"
|
||||||
|
|
||||||
git reset --hard
|
git config --global credential.helper store
|
||||||
git clean -fd
|
echo "https://${GIT_USERNAME}:${GIT_PASSWORD}@git.tech4biz.wiki" > ~/.git-credentials
|
||||||
git config pull.rebase false
|
|
||||||
git pull https://${GIT_USER}:${GIT_PASS}@git.tech4biz.wiki/yashwin/Qassure-frontend.git main
|
git clone -b ${GIT_BRANCH} ${GIT_REPO}
|
||||||
|
|
||||||
|
rm -f ~/.git-credentials
|
||||||
|
git config --global --unset credential.helper
|
||||||
|
echo "✅ Repository cloned successfully"
|
||||||
else
|
else
|
||||||
echo "Cloning fresh repo..."
|
echo "✅ Repository already exists"
|
||||||
sudo rm -rf ${DEPLOY_PATH}
|
fi
|
||||||
sudo mkdir -p /var/www
|
'
|
||||||
sudo git clone https://${GIT_USER}:${GIT_PASS}@git.tech4biz.wiki/yashwin/Qassure-frontend.git ${DEPLOY_PATH}
|
"""
|
||||||
sudo chown -R ubuntu:ubuntu ${DEPLOY_PATH}
|
}
|
||||||
git config --global --add safe.directory ${DEPLOY_PATH}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Fetch Latest Changes') {
|
||||||
|
steps {
|
||||||
|
sshagent(credentials: [SSH_KEY_CREDENTIALS_ID]) {
|
||||||
|
script {
|
||||||
|
withCredentials([usernamePassword(
|
||||||
|
credentialsId: GIT_CREDENTIALS_ID,
|
||||||
|
usernameVariable: 'GIT_USERNAME',
|
||||||
|
passwordVariable: 'GIT_PASSWORD'
|
||||||
|
)]) {
|
||||||
|
def gitInfo = sh(
|
||||||
|
script: """
|
||||||
|
ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} '
|
||||||
|
set -e
|
||||||
|
cd ${PROJECT_DIR}
|
||||||
|
|
||||||
|
echo "📥 Fetching latest changes..."
|
||||||
|
git config credential.helper store
|
||||||
|
echo "https://${GIT_USERNAME}:${GIT_PASSWORD}@git.tech4biz.wiki" > ~/.git-credentials
|
||||||
|
|
||||||
|
# Force fetch and reset to ensure we match remote exactly
|
||||||
|
git fetch origin ${GIT_BRANCH}
|
||||||
|
git reset --hard origin/${GIT_BRANCH}
|
||||||
|
|
||||||
|
rm -f ~/.git-credentials
|
||||||
|
git config --unset credential.helper
|
||||||
|
|
||||||
|
echo "COMMIT_SHORT=\$(git rev-parse --short HEAD)"
|
||||||
|
echo "COMMIT_MSG=\$(git log -1 --pretty=%B)"
|
||||||
|
echo "COMMIT_AUTHOR=\$(git log -1 --pretty=%an)"
|
||||||
|
'
|
||||||
|
""",
|
||||||
|
returnStdout: true
|
||||||
|
).trim()
|
||||||
|
|
||||||
|
gitInfo.split('\n').each { line ->
|
||||||
|
if (line.startsWith('COMMIT_SHORT=')) env.GIT_COMMIT_SHORT = line.split('=', 2)[1]
|
||||||
|
else if (line.startsWith('COMMIT_MSG=')) env.GIT_COMMIT_MSG = line.split('=', 2)[1]
|
||||||
|
else if (line.startsWith('COMMIT_AUTHOR=')) env.GIT_AUTHOR = line.split('=', 2)[1]
|
||||||
|
}
|
||||||
|
echo "✅ Updated to: ${env.GIT_COMMIT_SHORT}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Pre-Flight Checks') {
|
||||||
|
steps {
|
||||||
|
sshagent(credentials: [SSH_KEY_CREDENTIALS_ID]) {
|
||||||
|
script {
|
||||||
|
sh """
|
||||||
|
ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} '
|
||||||
|
set -e
|
||||||
|
echo "🔍 Running pre-flight checks..."
|
||||||
|
|
||||||
|
# Check Node.js
|
||||||
|
export PATH="${NODE_HOME}/bin:\$PATH"
|
||||||
|
if ! node --version >/dev/null 2>&1; then
|
||||||
|
echo "❌ Node.js not found at ${NODE_HOME}"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd ${DEPLOY_PATH}
|
# Disk Space (minimum 1GB)
|
||||||
echo "Commit: \$(git rev-parse HEAD)"
|
available=\$(df ${PROJECT_DIR} | tail -1 | awk "{print \\\$4}")
|
||||||
|
if [ \$available -lt 1048576 ]; then
|
||||||
|
echo "❌ Insufficient disk space"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Pre-flight checks passed"
|
||||||
'
|
'
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
stage('Verify Node.js Environment') {
|
stage('Install & Build') {
|
||||||
steps {
|
steps {
|
||||||
|
sshagent(credentials: [SSH_KEY_CREDENTIALS_ID]) {
|
||||||
script {
|
script {
|
||||||
sshagent(credentials: [SSH_CREDENTIALS]) {
|
|
||||||
sh """
|
sh """
|
||||||
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
|
ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} '
|
||||||
set -e
|
set -e
|
||||||
export PATH="/home/ubuntu/.nvm/versions/node/v22.17.0/bin:\$PATH"
|
cd ${PROJECT_DIR}
|
||||||
cd ${DEPLOY_PATH}
|
export PATH="${NODE_HOME}/bin:\$PATH"
|
||||||
|
export VITE_API_BASE_URL="${VITE_API_BASE_URL}"
|
||||||
|
|
||||||
echo "Node: \$(${NODE_PATH} -v)"
|
echo "📦 Installing dependencies..."
|
||||||
echo "NPM: \$(${NPM_PATH} -v)"
|
# Use clean install for reliability
|
||||||
${NPM_PATH} cache clean --force
|
npm ci --prefer-offline --no-audit
|
||||||
|
|
||||||
|
echo "🏗️ Building application..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Verify the build output
|
||||||
|
if [ -f "dist/index.html" ]; then
|
||||||
|
echo "✅ Build successful - dist/index.html optimized and ready"
|
||||||
|
else
|
||||||
|
echo "❌ Build failed - dist/index.html missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
'
|
'
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Install Dependencies') {
|
|
||||||
steps {
|
|
||||||
sshagent(credentials: [SSH_CREDENTIALS]) {
|
|
||||||
sh """
|
|
||||||
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
|
|
||||||
set -e
|
|
||||||
export PATH="/home/ubuntu/.nvm/versions/node/v22.17.0/bin:\$PATH"
|
|
||||||
cd ${DEPLOY_PATH}
|
|
||||||
echo "Installing dependencies..."
|
|
||||||
rm -rf node_modules package-lock.json
|
|
||||||
${NPM_PATH} install --force
|
|
||||||
'
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Build Application') {
|
|
||||||
steps {
|
|
||||||
sshagent(credentials: [SSH_CREDENTIALS]) {
|
|
||||||
sh """
|
|
||||||
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
|
|
||||||
set -e
|
|
||||||
export PATH="/home/ubuntu/.nvm/versions/node/v22.17.0/bin:\$PATH"
|
|
||||||
cd ${DEPLOY_PATH}
|
|
||||||
echo "Building..."
|
|
||||||
${NPM_PATH} run build
|
|
||||||
echo "Build directory contents:"
|
|
||||||
ls -la dist || ls -la build || echo "No build/dist directory found"
|
|
||||||
'
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Deploy Static Files') {
|
|
||||||
steps {
|
|
||||||
sshagent(credentials: [SSH_CREDENTIALS]) {
|
|
||||||
sh """
|
|
||||||
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
|
|
||||||
set -e
|
|
||||||
cd ${DEPLOY_PATH}
|
|
||||||
echo "Setting up static files for nginx..."
|
|
||||||
|
|
||||||
# Ensure proper ownership and permissions
|
|
||||||
sudo chown -R www-data:www-data ${DEPLOY_PATH}
|
|
||||||
sudo find ${DEPLOY_PATH} -type d -exec chmod 755 {} \\;
|
|
||||||
sudo find ${DEPLOY_PATH} -type f -exec chmod 644 {} \\;
|
|
||||||
|
|
||||||
echo "Static files prepared for nginx serving"
|
|
||||||
'
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Restart Nginx') {
|
|
||||||
steps {
|
|
||||||
sshagent(credentials: [SSH_CREDENTIALS]) {
|
|
||||||
sh """
|
|
||||||
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
|
|
||||||
set -e
|
|
||||||
echo "Testing nginx configuration..."
|
|
||||||
sudo nginx -t
|
|
||||||
|
|
||||||
echo "Restarting nginx..."
|
|
||||||
sudo systemctl restart nginx
|
|
||||||
|
|
||||||
echo "Checking nginx status..."
|
|
||||||
sudo systemctl status nginx --no-pager -l
|
|
||||||
'
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Health Check') {
|
|
||||||
steps {
|
|
||||||
sshagent(credentials: [SSH_CREDENTIALS]) {
|
|
||||||
sh """
|
|
||||||
ssh -o StrictHostKeyChecking=no ${REMOTE_SERVER} '
|
|
||||||
set -e
|
|
||||||
echo "Verifying deployment..."
|
|
||||||
echo "Directory structure:"
|
|
||||||
ls -la ${DEPLOY_PATH}
|
|
||||||
|
|
||||||
echo "Build files:"
|
|
||||||
ls -la ${DEPLOY_PATH}/dist || ls -la ${DEPLOY_PATH}/build || echo "No build directory found"
|
|
||||||
|
|
||||||
echo "Nginx status:"
|
|
||||||
sudo systemctl is-active nginx
|
|
||||||
'
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
always {
|
|
||||||
cleanWs()
|
|
||||||
}
|
|
||||||
success {
|
|
||||||
mail to: "${EMAIL_RECIPIENT}",
|
|
||||||
subject: "✅ Jenkins - ${PROJECT_NAME} Deployment Successful",
|
|
||||||
body: """The deployment of '${PROJECT_NAME}' to ${REMOTE_SERVER} was successful.
|
|
||||||
|
|
||||||
Build Number: ${BUILD_NUMBER}
|
|
||||||
URL: ${BUILD_URL}
|
|
||||||
Time: ${new Date()}
|
|
||||||
|
|
||||||
The static files have been deployed and nginx has been restarted.
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
failure {
|
failure {
|
||||||
mail to: "${EMAIL_RECIPIENT}",
|
script {
|
||||||
subject: "❌ Jenkins - ${PROJECT_NAME} Deployment Failed",
|
echo "❌ Deployment failed"
|
||||||
body: """Deployment failed. Please review logs at:
|
def durationMillis = System.currentTimeMillis() - env.DEPLOY_START_TIME.toLong()
|
||||||
|
def minutes = (durationMillis / 60000) as Integer
|
||||||
|
def seconds = ((durationMillis % 60000) / 1000) as Integer
|
||||||
|
|
||||||
${BUILD_URL}console
|
def failureMessage = """❌ QAssur Frontend DEPLOYMENT FAILED
|
||||||
|
|
||||||
Time: ${new Date()}
|
═══════════════════════════════════════
|
||||||
|
DETAILS
|
||||||
|
═══════════════════════════════════════
|
||||||
|
Branch: ${GIT_BRANCH}
|
||||||
|
Commit: ${env.GIT_COMMIT_SHORT ?: 'Unknown'}
|
||||||
|
Author: ${env.GIT_AUTHOR ?: 'Unknown'}
|
||||||
|
Duration: ${minutes}m ${seconds}s
|
||||||
|
Stage: ${env.STAGE_NAME ?: 'Unknown'}
|
||||||
|
Server: ${SERVER_IP}
|
||||||
|
|
||||||
|
Action Required:
|
||||||
|
1. Check Jenkins console logs for detailed error
|
||||||
|
2. Verify local build works: npm run build
|
||||||
"""
|
"""
|
||||||
|
try {
|
||||||
|
mail to: "${EMAIL_RECIPIENTS}",
|
||||||
|
subject: "❌ QAssur Frontend FAILED - Build #${BUILD_NUMBER}",
|
||||||
|
body: failureMessage
|
||||||
|
} catch (e) {
|
||||||
|
echo "⚠️ Email failed: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
success {
|
||||||
|
script {
|
||||||
|
def durationMillis = System.currentTimeMillis() - env.DEPLOY_START_TIME.toLong()
|
||||||
|
def minutes = (durationMillis / 60000) as Integer
|
||||||
|
def seconds = ((durationMillis % 60000) / 1000) as Integer
|
||||||
|
|
||||||
|
def successMessage = """✅ QAssur Frontend DEPLOYMENT SUCCESS
|
||||||
|
|
||||||
|
═══════════════════════════════════════
|
||||||
|
DETAILS
|
||||||
|
═══════════════════════════════════════
|
||||||
|
Branch: ${GIT_BRANCH}
|
||||||
|
Commit: ${env.GIT_COMMIT_SHORT}
|
||||||
|
Author: ${env.GIT_AUTHOR}
|
||||||
|
Duration: ${minutes}m ${seconds}s
|
||||||
|
|
||||||
|
Live URL: https://qasure.tech4bizsolutions.com
|
||||||
|
"""
|
||||||
|
try {
|
||||||
|
mail to: "${EMAIL_RECIPIENTS}",
|
||||||
|
subject: "✅ QAssur Frontend SUCCESS - Build #${BUILD_NUMBER}",
|
||||||
|
body: successMessage
|
||||||
|
} catch (e) {
|
||||||
|
echo "⚠️ Email failed: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 { Toaster } from "sonner";
|
||||||
import Login from "./pages/Login";
|
import { AppRoutes } from "@/routes";
|
||||||
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";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Toaster position="top-right" richColors />
|
<Toaster position="top-right" richColors />
|
||||||
<Routes>
|
<AppRoutes />
|
||||||
<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>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,7 +102,7 @@ const subscriptionTierOptions = [
|
|||||||
|
|
||||||
// Helper function to get base URL without protocol
|
// Helper function to get base URL without protocol
|
||||||
const getBaseUrlWithoutProtocol = (): string => {
|
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://)
|
// Remove protocol (http:// or https://)
|
||||||
return apiBaseUrl.replace(/^https?:\/\//, '');
|
return apiBaseUrl.replace(/^https?:\/\//, '');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,7 +29,7 @@ type LoginFormData = z.infer<typeof loginSchema>;
|
|||||||
const Login = (): ReactElement => {
|
const Login = (): ReactElement => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { isLoading, error, isAuthenticated } = useAppSelector((state) => state.auth);
|
const { isLoading, error, isAuthenticated, roles } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -47,9 +47,26 @@ const Login = (): ReactElement => {
|
|||||||
// Redirect if already authenticated
|
// Redirect if already authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
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 = [];
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, navigate]);
|
}
|
||||||
|
if (rolesArray.includes('super_admin')) {
|
||||||
|
navigate('/dashboard');
|
||||||
|
} else {
|
||||||
|
// Tenant admin - redirect to tenant dashboard
|
||||||
|
navigate('/tenant/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, roles, navigate]);
|
||||||
|
|
||||||
// Clear errors only on component mount, not on every auth state change
|
// Clear errors only on component mount, not on every auth state change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -72,8 +89,14 @@ const Login = (): ReactElement => {
|
|||||||
const message = result.message || 'Login successful';
|
const message = result.message || 'Login successful';
|
||||||
const description = result.message ? undefined : 'Welcome back!';
|
const description = result.message ? undefined : 'Welcome back!';
|
||||||
showToast.success(message, description);
|
showToast.success(message, description);
|
||||||
// Only navigate on success
|
|
||||||
|
// Check roles after login to redirect appropriately
|
||||||
|
const userRoles = result.data.roles || [];
|
||||||
|
if (userRoles.includes('super_admin')) {
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
|
} else {
|
||||||
|
navigate('/tenant/dashboard');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Clear Redux error state since we're handling errors locally
|
// Clear Redux error state since we're handling errors locally
|
||||||
@ -140,7 +163,7 @@ const Login = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl md:text-3xl font-bold text-[#0f1724] mb-2">
|
<h1 className="text-2xl md:text-3xl font-bold text-[#0f1724] mb-2">
|
||||||
Welcome Back 1
|
Welcome Back
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm md:text-base text-[#6b7280]">
|
<p className="text-sm md:text-base text-[#6b7280]">
|
||||||
Sign in to your account to continue
|
Sign in to your account to continue
|
||||||
|
|||||||
@ -7,9 +7,32 @@ interface ProtectedRouteProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }: ProtectedRouteProps): ReactElement => {
|
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;
|
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