Major Enhancement: NestJS Migration + CASL Authorization + Error Handling
Some checks failed
CI/CD Pipeline / Backend Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Tests (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Backend Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Tests (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
Complete rewrite from Express to NestJS with enterprise-grade features: ## Backend Improvements - Migrated from Express to NestJS 11.0.1 with TypeScript - Implemented Prisma ORM 7.3.0 for type-safe database access - Added CASL authorization system replacing role-based guards - Created global exception filters with structured logging - Implemented Auth0 JWT authentication with Passport.js - Added vehicle management with conflict detection - Enhanced event scheduling with driver/vehicle assignment - Comprehensive error handling and logging ## Frontend Improvements - Upgraded to React 19.2.0 with Vite 7.2.4 - Implemented CASL-based permission system - Added AbilityContext for declarative permissions - Created ErrorHandler utility for consistent error messages - Enhanced API client with request/response logging - Added War Room (Command Center) dashboard - Created VIP Schedule view with complete itineraries - Implemented Vehicle Management UI - Added mock data generators for testing (288 events across 20 VIPs) ## New Features - Vehicle fleet management (types, capacity, status tracking) - Complete 3-day Jamboree schedule generation - Individual VIP schedule pages with PDF export (planned) - Real-time War Room dashboard with auto-refresh - Permission-based navigation filtering - First user auto-approval as administrator ## Documentation - Created CASL_AUTHORIZATION.md (comprehensive guide) - Created ERROR_HANDLING.md (error handling patterns) - Updated CLAUDE.md with new architecture - Added migration guides and best practices ## Technical Debt Resolved - Removed custom authentication in favor of Auth0 - Replaced role checks with CASL abilities - Standardized error responses across API - Implemented proper TypeScript typing - Added comprehensive logging Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,176 +1,119 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
||||
import { apiCall } from './config/api';
|
||||
import VipList from './pages/VipList';
|
||||
import VipDetails from './pages/VipDetails';
|
||||
import DriverList from './pages/DriverList';
|
||||
import DriverDashboard from './pages/DriverDashboard';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import AdminDashboard from './pages/AdminDashboard';
|
||||
import UserManagement from './components/UserManagement';
|
||||
import Login from './components/Login';
|
||||
import './App.css';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Auth0Provider } from '@auth0/auth0-react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { AuthProvider } from '@/contexts/AuthContext';
|
||||
import { AbilityProvider } from '@/contexts/AbilityContext';
|
||||
import { ProtectedRoute } from '@/components/ProtectedRoute';
|
||||
import { Layout } from '@/components/Layout';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
import { Login } from '@/pages/Login';
|
||||
import { Callback } from '@/pages/Callback';
|
||||
import { PendingApproval } from '@/pages/PendingApproval';
|
||||
import { Dashboard } from '@/pages/Dashboard';
|
||||
import { CommandCenter } from '@/pages/CommandCenter';
|
||||
import { VIPList } from '@/pages/VIPList';
|
||||
import { VIPSchedule } from '@/pages/VIPSchedule';
|
||||
import { DriverList } from '@/pages/DriverList';
|
||||
import { VehicleList } from '@/pages/VehicleList';
|
||||
import { EventList } from '@/pages/EventList';
|
||||
import { FlightList } from '@/pages/FlightList';
|
||||
import { UserList } from '@/pages/UserList';
|
||||
import { AdminTools } from '@/pages/AdminTools';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const domain = import.meta.env.VITE_AUTH0_DOMAIN;
|
||||
const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID;
|
||||
const audience = import.meta.env.VITE_AUTH0_AUDIENCE;
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user is already authenticated
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (token) {
|
||||
apiCall('/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
} else {
|
||||
// Token is invalid, remove it
|
||||
localStorage.removeItem('authToken');
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
})
|
||||
.then(userData => {
|
||||
setUser(userData);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Auth check failed:', error);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleLogin = (userData: any) => {
|
||||
setUser(userData);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('authToken');
|
||||
setUser(null);
|
||||
// Optionally call logout endpoint
|
||||
apiCall('/auth/logout', { method: 'POST' })
|
||||
.catch(error => console.error('Logout error:', error));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 flex items-center space-x-4">
|
||||
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span className="text-lg font-medium text-slate-700">Loading VIP Coordinator...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle OAuth callback route even when not logged in
|
||||
if (window.location.pathname === '/auth/callback' || window.location.pathname === '/auth/google/callback') {
|
||||
return <Login onLogin={handleLogin} />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Login onLogin={handleLogin} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50">
|
||||
{/* Modern Navigation */}
|
||||
<nav className="bg-white/80 backdrop-blur-lg border-b border-slate-200/60 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo/Brand */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">VC</span>
|
||||
</div>
|
||||
<h1 className="text-xl font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
|
||||
VIP Coordinator
|
||||
</h1>
|
||||
</div>
|
||||
<ErrorBoundary>
|
||||
<Auth0Provider
|
||||
domain={domain}
|
||||
clientId={clientId}
|
||||
authorizationParams={{
|
||||
redirect_uri: `${window.location.origin}/callback`,
|
||||
audience: audience,
|
||||
}}
|
||||
useRefreshTokens={true}
|
||||
cacheLocation="localstorage"
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<AbilityProvider>
|
||||
<BrowserRouter
|
||||
future={{
|
||||
v7_startTransition: true,
|
||||
v7_relativeSplatPath: true,
|
||||
}}
|
||||
>
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#333',
|
||||
color: '#fff',
|
||||
},
|
||||
success: {
|
||||
duration: 3000,
|
||||
iconTheme: {
|
||||
primary: '#10b981',
|
||||
secondary: '#fff',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
duration: 5000,
|
||||
iconTheme: {
|
||||
primary: '#ef4444',
|
||||
secondary: '#fff',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route path="/pending-approval" element={<PendingApproval />} />
|
||||
|
||||
{/* Navigation Links */}
|
||||
<div className="hidden md:flex items-center space-x-1">
|
||||
<Link
|
||||
to="/"
|
||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/vips"
|
||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
|
||||
>
|
||||
VIPs
|
||||
</Link>
|
||||
<Link
|
||||
to="/drivers"
|
||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
|
||||
>
|
||||
Drivers
|
||||
</Link>
|
||||
{(user.role === 'administrator' || user.role === 'coordinator') && (
|
||||
<Link
|
||||
to="/admin"
|
||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-all duration-200"
|
||||
>
|
||||
Admin
|
||||
</Link>
|
||||
)}
|
||||
{user.role === 'administrator' && (
|
||||
<Link
|
||||
to="/users"
|
||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-all duration-200"
|
||||
>
|
||||
Users
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User Menu */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="hidden sm:flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-slate-400 to-slate-600 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-xs font-medium">
|
||||
{user.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-slate-900">{user.name}</div>
|
||||
<div className="text-slate-500 capitalize">{user.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-6 lg:px-8 py-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/vips" element={<VipList />} />
|
||||
<Route path="/vips/:id" element={<VipDetails />} />
|
||||
<Route path="/drivers" element={<DriverList />} />
|
||||
<Route path="/drivers/:driverId" element={<DriverDashboard />} />
|
||||
<Route path="/admin" element={<AdminDashboard />} />
|
||||
<Route path="/users" element={<UserManagement currentUser={user} />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</Router>
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/command-center" element={<CommandCenter />} />
|
||||
<Route path="/vips" element={<VIPList />} />
|
||||
<Route path="/vips/:id/schedule" element={<VIPSchedule />} />
|
||||
<Route path="/drivers" element={<DriverList />} />
|
||||
<Route path="/vehicles" element={<VehicleList />} />
|
||||
<Route path="/events" element={<EventList />} />
|
||||
<Route path="/flights" element={<FlightList />} />
|
||||
<Route path="/users" element={<UserList />} />
|
||||
<Route path="/admin-tools" element={<AdminTools />} />
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AbilityProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
</Auth0Provider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user