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:
257
frontend-old-20260125/src/components/UserOnboarding.tsx
Normal file
257
frontend-old-20260125/src/components/UserOnboarding.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { apiCall } from '../utils/api';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { LoadingSpinner } from './LoadingSpinner';
|
||||
|
||||
interface OnboardingData {
|
||||
requestedRole: 'coordinator' | 'driver' | 'viewer';
|
||||
phone: string;
|
||||
organization: string;
|
||||
reason: string;
|
||||
// Driver-specific fields
|
||||
vehicleType?: string;
|
||||
vehicleCapacity?: number;
|
||||
licensePlate?: string;
|
||||
}
|
||||
|
||||
const UserOnboarding: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { showToast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState<OnboardingData>({
|
||||
requestedRole: 'viewer',
|
||||
phone: '',
|
||||
organization: '',
|
||||
reason: '',
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const response = await apiCall('/api/users/complete-onboarding', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
onboardingData: formData,
|
||||
phone: formData.phone,
|
||||
organization: formData.organization,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast('Onboarding completed! Your account is pending approval.', 'success');
|
||||
navigate('/pending-approval');
|
||||
} else {
|
||||
showToast('Failed to complete onboarding. Please try again.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('An error occurred. Please try again.', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleChange = (role: 'coordinator' | 'driver' | 'viewer') => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
requestedRole: role,
|
||||
// Clear driver fields if not driver
|
||||
vehicleType: role === 'driver' ? prev.vehicleType : undefined,
|
||||
vehicleCapacity: role === 'driver' ? prev.vehicleCapacity : undefined,
|
||||
licensePlate: role === 'driver' ? prev.licensePlate : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl shadow-xl max-w-2xl w-full p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-2">Welcome to VIP Coordinator</h1>
|
||||
<p className="text-slate-600">Please complete your profile to request access</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Role Selection */}
|
||||
<div className="form-section">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-3">
|
||||
What type of access do you need?
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRoleChange('coordinator')}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
formData.requestedRole === 'coordinator'
|
||||
? 'border-amber-500 bg-amber-50'
|
||||
: 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl mb-2">📋</div>
|
||||
<div className="font-semibold text-slate-800">Coordinator</div>
|
||||
<div className="text-xs text-slate-600 mt-1">Manage VIPs & schedules</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRoleChange('driver')}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
formData.requestedRole === 'driver'
|
||||
? 'border-amber-500 bg-amber-50'
|
||||
: 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl mb-2">🚗</div>
|
||||
<div className="font-semibold text-slate-800">Driver</div>
|
||||
<div className="text-xs text-slate-600 mt-1">Transport VIPs</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRoleChange('viewer')}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
formData.requestedRole === 'viewer'
|
||||
? 'border-amber-500 bg-amber-50'
|
||||
: 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="text-2xl mb-2">👁️</div>
|
||||
<div className="font-semibold text-slate-800">Viewer</div>
|
||||
<div className="text-xs text-slate-600 mt-1">View-only access</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common Fields */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Phone Number *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, phone: e.target.value }))}
|
||||
className="form-input w-full"
|
||||
placeholder="+1 (555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Organization *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.organization}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, organization: e.target.value }))}
|
||||
className="form-input w-full"
|
||||
placeholder="Your company or department"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Driver-specific Fields */}
|
||||
{formData.requestedRole === 'driver' && (
|
||||
<div className="space-y-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<h3 className="font-semibold text-slate-800 mb-3">Driver Information</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Vehicle Type *
|
||||
</label>
|
||||
<select
|
||||
required
|
||||
value={formData.vehicleType || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, vehicleType: e.target.value }))}
|
||||
className="form-select w-full"
|
||||
>
|
||||
<option value="">Select vehicle type</option>
|
||||
<option value="sedan">Sedan</option>
|
||||
<option value="suv">SUV</option>
|
||||
<option value="van">Van</option>
|
||||
<option value="minibus">Minibus</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Passenger Capacity *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
min="1"
|
||||
max="20"
|
||||
value={formData.vehicleCapacity || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, vehicleCapacity: parseInt(e.target.value) }))}
|
||||
className="form-input w-full"
|
||||
placeholder="4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
License Plate *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.licensePlate || ''}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, licensePlate: e.target.value }))}
|
||||
className="form-input w-full"
|
||||
placeholder="ABC-1234"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reason for Access */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Why do you need access? *
|
||||
</label>
|
||||
<textarea
|
||||
required
|
||||
rows={3}
|
||||
value={formData.reason}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, reason: e.target.value }))}
|
||||
className="form-textarea w-full"
|
||||
placeholder="Please explain your role and why you need access to the VIP Coordinator system..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/')}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{loading ? <LoadingSpinner size="sm" /> : 'Submit Request'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserOnboarding;
|
||||
Reference in New Issue
Block a user