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

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:
2026-01-31 08:50:25 +01:00
parent 8ace1ab2c1
commit 868f7efc23
351 changed files with 44997 additions and 6276 deletions

View File

@@ -0,0 +1,534 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireAuth = requireAuth;
exports.requireRole = requireRole;
const express_1 = __importDefault(require("express"));
const simpleAuth_1 = require("../config/simpleAuth");
const databaseService_1 = __importDefault(require("../services/databaseService"));
const router = express_1.default.Router();
// Enhanced logging for production debugging
function logAuthEvent(event, details = {}) {
const timestamp = new Date().toISOString();
console.log(`🔐 [AUTH ${timestamp}] ${event}:`, JSON.stringify(details, null, 2));
}
// Validate environment variables on startup
function validateAuthEnvironment() {
const required = ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'GOOGLE_REDIRECT_URI', 'FRONTEND_URL'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
logAuthEvent('ENVIRONMENT_ERROR', { missing_variables: missing });
return false;
}
// Validate URLs
const frontendUrl = process.env.FRONTEND_URL;
const redirectUri = process.env.GOOGLE_REDIRECT_URI;
if (!frontendUrl?.startsWith('http')) {
logAuthEvent('ENVIRONMENT_ERROR', { error: 'FRONTEND_URL must start with http/https' });
return false;
}
if (!redirectUri?.startsWith('http')) {
logAuthEvent('ENVIRONMENT_ERROR', { error: 'GOOGLE_REDIRECT_URI must start with http/https' });
return false;
}
logAuthEvent('ENVIRONMENT_VALIDATED', {
frontend_url: frontendUrl,
redirect_uri: redirectUri,
client_id_configured: !!process.env.GOOGLE_CLIENT_ID,
client_secret_configured: !!process.env.GOOGLE_CLIENT_SECRET
});
return true;
}
// Validate environment on module load
const isEnvironmentValid = validateAuthEnvironment();
// Middleware to check authentication
function requireAuth(req, res, next) {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
logAuthEvent('AUTH_FAILED', {
reason: 'no_token',
ip: req.ip,
path: req.path,
headers_present: !!req.headers.authorization
});
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
if (!token || token.length < 10) {
logAuthEvent('AUTH_FAILED', {
reason: 'invalid_token_format',
ip: req.ip,
path: req.path,
token_length: token?.length || 0
});
return res.status(401).json({ error: 'Invalid token format' });
}
const user = (0, simpleAuth_1.verifyToken)(token);
if (!user) {
logAuthEvent('AUTH_FAILED', {
reason: 'token_verification_failed',
ip: req.ip,
path: req.path,
token_prefix: token.substring(0, 10) + '...'
});
return res.status(401).json({ error: 'Invalid or expired token' });
}
logAuthEvent('AUTH_SUCCESS', {
user_id: user.id,
user_email: user.email,
user_role: user.role,
ip: req.ip,
path: req.path
});
req.user = user;
next();
}
catch (error) {
logAuthEvent('AUTH_ERROR', {
error: error instanceof Error ? error.message : 'Unknown error',
ip: req.ip,
path: req.path
});
return res.status(500).json({ error: 'Authentication system error' });
}
}
// Middleware to check role
function requireRole(roles) {
return (req, res, next) => {
const user = req.user;
if (!user || !roles.includes(user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Get current user
router.get('/me', requireAuth, (req, res) => {
res.json(req.user);
});
// Setup status endpoint (required by frontend)
router.get('/setup', async (req, res) => {
try {
const clientId = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
const redirectUri = process.env.GOOGLE_REDIRECT_URI;
const frontendUrl = process.env.FRONTEND_URL;
logAuthEvent('SETUP_CHECK', {
client_id_present: !!clientId,
client_secret_present: !!clientSecret,
redirect_uri_present: !!redirectUri,
frontend_url_present: !!frontendUrl,
environment_valid: isEnvironmentValid
});
// Check database connectivity
let userCount = 0;
let databaseConnected = false;
try {
userCount = await databaseService_1.default.getUserCount();
databaseConnected = true;
logAuthEvent('DATABASE_CHECK', { status: 'connected', user_count: userCount });
}
catch (dbError) {
logAuthEvent('DATABASE_ERROR', {
error: dbError instanceof Error ? dbError.message : 'Unknown database error'
});
return res.status(500).json({
error: 'Database connection failed',
details: 'Cannot connect to PostgreSQL database'
});
}
const setupCompleted = !!(clientId &&
clientSecret &&
redirectUri &&
frontendUrl &&
clientId !== 'your-google-client-id-from-console' &&
clientId !== 'your-google-client-id' &&
isEnvironmentValid);
const response = {
setupCompleted,
firstAdminCreated: userCount > 0,
oauthConfigured: !!(clientId && clientSecret),
databaseConnected,
environmentValid: isEnvironmentValid,
configuration: {
google_oauth: !!(clientId && clientSecret),
redirect_uri_configured: !!redirectUri,
frontend_url_configured: !!frontendUrl,
production_ready: setupCompleted && databaseConnected
}
};
logAuthEvent('SETUP_STATUS', response);
res.json(response);
}
catch (error) {
logAuthEvent('SETUP_ERROR', {
error: error instanceof Error ? error.message : 'Unknown setup error'
});
res.status(500).json({
error: 'Setup check failed',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Start Google OAuth flow
router.get('/google', (req, res) => {
try {
const authUrl = (0, simpleAuth_1.getGoogleAuthUrl)();
res.redirect(authUrl);
}
catch (error) {
console.error('Error starting Google OAuth:', error);
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
res.redirect(`${frontendUrl}?error=oauth_not_configured`);
}
});
// Handle Google OAuth callback (this is where Google redirects back to)
router.get('/google/callback', async (req, res) => {
const { code, error, state } = req.query;
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
logAuthEvent('OAUTH_CALLBACK', {
has_code: !!code,
has_error: !!error,
error_type: error,
state,
frontend_url: frontendUrl,
ip: req.ip,
user_agent: req.get('User-Agent')
});
// Validate environment before proceeding
if (!isEnvironmentValid) {
logAuthEvent('OAUTH_CALLBACK_ERROR', { reason: 'invalid_environment' });
return res.redirect(`${frontendUrl}?error=configuration_error&message=OAuth not properly configured`);
}
if (error) {
logAuthEvent('OAUTH_ERROR', { error, ip: req.ip });
return res.redirect(`${frontendUrl}?error=${error}&message=OAuth authorization failed`);
}
if (!code) {
logAuthEvent('OAUTH_ERROR', { reason: 'no_authorization_code', ip: req.ip });
return res.redirect(`${frontendUrl}?error=no_code&message=No authorization code received`);
}
try {
logAuthEvent('OAUTH_TOKEN_EXCHANGE_START', { code_length: code.length });
// Exchange code for tokens
const tokens = await (0, simpleAuth_1.exchangeCodeForTokens)(code);
if (!tokens || !tokens.access_token) {
logAuthEvent('OAUTH_TOKEN_EXCHANGE_FAILED', { tokens_received: !!tokens });
return res.redirect(`${frontendUrl}?error=token_exchange_failed&message=Failed to exchange authorization code`);
}
logAuthEvent('OAUTH_TOKEN_EXCHANGE_SUCCESS', { has_access_token: !!tokens.access_token });
// Get user info
const googleUser = await (0, simpleAuth_1.getGoogleUserInfo)(tokens.access_token);
if (!googleUser || !googleUser.email) {
logAuthEvent('OAUTH_USER_INFO_FAILED', { user_data: !!googleUser });
return res.redirect(`${frontendUrl}?error=user_info_failed&message=Failed to get user information from Google`);
}
logAuthEvent('OAUTH_USER_INFO_SUCCESS', {
email: googleUser.email,
name: googleUser.name,
verified_email: googleUser.verified_email
});
// Check if user exists or create new user
let user = await databaseService_1.default.getUserByEmail(googleUser.email);
if (!user) {
// Determine role - first user becomes admin, others need approval
const approvedUserCount = await databaseService_1.default.getApprovedUserCount();
const role = approvedUserCount === 0 ? 'administrator' : 'coordinator';
logAuthEvent('USER_CREATION', {
email: googleUser.email,
role,
is_first_user: approvedUserCount === 0
});
user = await databaseService_1.default.createUser({
id: googleUser.id,
google_id: googleUser.id,
email: googleUser.email,
name: googleUser.name,
profile_picture_url: googleUser.picture,
role
});
// Auto-approve first admin, others need approval
if (approvedUserCount === 0) {
await databaseService_1.default.updateUserApprovalStatus(googleUser.email, 'approved');
user.approval_status = 'approved';
logAuthEvent('FIRST_ADMIN_CREATED', { email: googleUser.email });
}
else {
logAuthEvent('USER_PENDING_APPROVAL', { email: googleUser.email });
}
}
else {
// Update last sign in
await databaseService_1.default.updateUserLastSignIn(googleUser.email);
logAuthEvent('USER_LOGIN', {
email: user.email,
name: user.name,
role: user.role,
approval_status: user.approval_status
});
}
// Check if user is approved
if (user.approval_status !== 'approved') {
logAuthEvent('USER_NOT_APPROVED', { email: user.email, status: user.approval_status });
return res.redirect(`${frontendUrl}?error=pending_approval&message=Your account is pending administrator approval`);
}
// Generate JWT token
const token = (0, simpleAuth_1.generateToken)(user);
logAuthEvent('JWT_TOKEN_GENERATED', {
user_id: user.id,
email: user.email,
role: user.role,
token_length: token.length
});
// Redirect to frontend with token
const callbackUrl = `${frontendUrl}/auth/callback?token=${token}`;
logAuthEvent('OAUTH_SUCCESS_REDIRECT', { callback_url: callbackUrl });
res.redirect(callbackUrl);
}
catch (error) {
logAuthEvent('OAUTH_CALLBACK_ERROR', {
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
ip: req.ip
});
res.redirect(`${frontendUrl}?error=oauth_failed&message=Authentication failed due to server error`);
}
});
// Exchange OAuth code for JWT token (alternative endpoint for frontend)
router.post('/google/exchange', async (req, res) => {
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: 'Authorization code is required' });
}
try {
// Exchange code for tokens
const tokens = await (0, simpleAuth_1.exchangeCodeForTokens)(code);
// Get user info
const googleUser = await (0, simpleAuth_1.getGoogleUserInfo)(tokens.access_token);
// Check if user exists or create new user
let user = await databaseService_1.default.getUserByEmail(googleUser.email);
if (!user) {
// Determine role - first user becomes admin
const userCount = await databaseService_1.default.getUserCount();
const role = userCount === 0 ? 'administrator' : 'coordinator';
user = await databaseService_1.default.createUser({
id: googleUser.id,
google_id: googleUser.id,
email: googleUser.email,
name: googleUser.name,
profile_picture_url: googleUser.picture,
role
});
}
else {
// Update last sign in
await databaseService_1.default.updateUserLastSignIn(googleUser.email);
console.log(`✅ User logged in: ${user.name} (${user.email})`);
}
// Generate JWT token
const token = (0, simpleAuth_1.generateToken)(user);
// Return token to frontend
res.json({
token,
user: {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role
}
});
}
catch (error) {
console.error('Error in OAuth exchange:', error);
res.status(500).json({ error: 'Failed to exchange authorization code' });
}
});
// Get OAuth URL for frontend to redirect to
router.get('/google/url', (req, res) => {
try {
const authUrl = (0, simpleAuth_1.getGoogleAuthUrl)();
res.json({ url: authUrl });
}
catch (error) {
console.error('Error getting Google OAuth URL:', error);
res.status(500).json({ error: 'OAuth not configured' });
}
});
// Logout
router.post('/logout', (req, res) => {
// With JWT, logout is handled client-side by removing the token
res.json({ message: 'Logged out successfully' });
});
// Get auth status
router.get('/status', (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.json({ authenticated: false });
}
const token = authHeader.substring(7);
const user = (0, simpleAuth_1.verifyToken)(token);
if (!user) {
return res.json({ authenticated: false });
}
res.json({
authenticated: true,
user: {
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role
}
});
});
// USER MANAGEMENT ENDPOINTS
// List all users (admin only)
router.get('/users', requireAuth, requireRole(['administrator']), async (req, res) => {
try {
const users = await databaseService_1.default.getAllUsers();
const userList = users.map(user => ({
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
created_at: user.created_at,
last_login: user.last_login,
provider: 'google'
}));
res.json(userList);
}
catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ error: 'Failed to fetch users' });
}
});
// Update user role (admin only)
router.patch('/users/:email/role', requireAuth, requireRole(['administrator']), async (req, res) => {
const { email } = req.params;
const { role } = req.body;
if (!['administrator', 'coordinator', 'driver'].includes(role)) {
return res.status(400).json({ error: 'Invalid role' });
}
try {
const user = await databaseService_1.default.updateUserRole(email, role);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role
}
});
}
catch (error) {
console.error('Error updating user role:', error);
res.status(500).json({ error: 'Failed to update user role' });
}
});
// Delete user (admin only)
router.delete('/users/:email', requireAuth, requireRole(['administrator']), async (req, res) => {
const { email } = req.params;
const currentUser = req.user;
// Prevent admin from deleting themselves
if (email === currentUser.email) {
return res.status(400).json({ error: 'Cannot delete your own account' });
}
try {
const deletedUser = await databaseService_1.default.deleteUser(email);
if (!deletedUser) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ success: true, message: 'User deleted successfully' });
}
catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({ error: 'Failed to delete user' });
}
});
// Get user by email (admin only)
router.get('/users/:email', requireAuth, requireRole(['administrator']), async (req, res) => {
const { email } = req.params;
try {
const user = await databaseService_1.default.getUserByEmail(email);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
created_at: user.created_at,
last_login: user.last_login,
provider: 'google',
approval_status: user.approval_status
});
}
catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Failed to fetch user' });
}
});
// USER APPROVAL ENDPOINTS
// Get pending users (admin only)
router.get('/users/pending/list', requireAuth, requireRole(['administrator']), async (req, res) => {
try {
const pendingUsers = await databaseService_1.default.getPendingUsers();
const userList = pendingUsers.map(user => ({
id: user.id,
email: user.email,
name: user.name,
picture: user.profile_picture_url,
role: user.role,
created_at: user.created_at,
provider: 'google',
approval_status: user.approval_status
}));
res.json(userList);
}
catch (error) {
console.error('Error fetching pending users:', error);
res.status(500).json({ error: 'Failed to fetch pending users' });
}
});
// Approve or deny user (admin only)
router.patch('/users/:email/approval', requireAuth, requireRole(['administrator']), async (req, res) => {
const { email } = req.params;
const { status } = req.body;
if (!['approved', 'denied'].includes(status)) {
return res.status(400).json({ error: 'Invalid approval status. Must be "approved" or "denied"' });
}
try {
const user = await databaseService_1.default.updateUserApprovalStatus(email, status);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
success: true,
message: `User ${status} successfully`,
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
approval_status: user.approval_status
}
});
}
catch (error) {
console.error('Error updating user approval:', error);
res.status(500).json({ error: 'Failed to update user approval' });
}
});
exports.default = router;
//# sourceMappingURL=simpleAuth.js.map