Compare commits
19 Commits
v0.1.0-pro
...
689b89ea83
| Author | SHA1 | Date | |
|---|---|---|---|
| 689b89ea83 | |||
| b8fac5de23 | |||
| 6c3f017a9e | |||
| 9e9d4245bb | |||
| 147078d72f | |||
| 4d31e16381 | |||
| 440884666d | |||
| e8987d5970 | |||
| d3e08cd04c | |||
| ba5aa4731a | |||
| d2754db377 | |||
| 868f7efc23 | |||
| 8ace1ab2c1 | |||
| 36cb8e8886 | |||
| 035f76fdd3 | |||
| 542cfe0878 | |||
| a0f001ecb1 | |||
| dc4655cef4 | |||
| 8fb00ec041 |
29
.env.prod
29
.env.prod
@@ -1,29 +0,0 @@
|
||||
# Production Environment Configuration - SECURE VALUES
|
||||
|
||||
# Database Configuration
|
||||
DB_PASSWORD=VipCoord2025SecureDB!
|
||||
|
||||
# Domain Configuration
|
||||
DOMAIN=bsa.madeamess.online
|
||||
VITE_API_URL=https://api.bsa.madeamess.online
|
||||
|
||||
# Authentication Configuration (Secure production keys)
|
||||
JWT_SECRET=VipCoord2025JwtSecretKey8f9a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z
|
||||
SESSION_SECRET=VipCoord2025SessionSecret9g8f7e6d5c4b3a2z1y0x9w8v7u6t5s4r3q2p1o0n9m8l7k6j5i4h3g2f1e
|
||||
|
||||
# Google OAuth Configuration
|
||||
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=https://bsa.madeamess.online
|
||||
|
||||
# Flight API Configuration
|
||||
AVIATIONSTACK_API_KEY=your-aviationstack-api-key
|
||||
|
||||
# Admin Configuration
|
||||
ADMIN_PASSWORD=VipAdmin2025Secure!
|
||||
|
||||
# Port Configuration
|
||||
PORT=3000
|
||||
@@ -1,30 +0,0 @@
|
||||
# Production Environment Configuration
|
||||
# Copy this file to .env.prod and update the values for your production deployment
|
||||
|
||||
# Database Configuration
|
||||
DB_PASSWORD=your-secure-database-password-here
|
||||
|
||||
# Domain Configuration
|
||||
DOMAIN=bsa.madeamess.online
|
||||
VITE_API_URL=https://api.bsa.madeamess.online/api
|
||||
|
||||
# Authentication Configuration (Generate new secure keys for production)
|
||||
JWT_SECRET=your-super-secure-jwt-secret-key-change-in-production-12345
|
||||
SESSION_SECRET=your-super-secure-session-secret-change-in-production-67890
|
||||
|
||||
# Google OAuth Configuration
|
||||
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=https://bsa.madeamess.online
|
||||
|
||||
# Flight API Configuration
|
||||
AVIATIONSTACK_API_KEY=your-aviationstack-api-key
|
||||
|
||||
# Admin Configuration
|
||||
ADMIN_PASSWORD=your-secure-admin-password
|
||||
|
||||
# Port Configuration
|
||||
PORT=3000
|
||||
83
.env.production.example
Normal file
83
.env.production.example
Normal file
@@ -0,0 +1,83 @@
|
||||
# ==========================================
|
||||
# VIP Coordinator - Production Environment
|
||||
# ==========================================
|
||||
# Copy this file to .env.production and fill in your values
|
||||
# DO NOT commit .env.production to version control
|
||||
|
||||
# ==========================================
|
||||
# Database Configuration
|
||||
# ==========================================
|
||||
POSTGRES_DB=vip_coordinator
|
||||
POSTGRES_USER=vip_user
|
||||
POSTGRES_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD
|
||||
|
||||
# ==========================================
|
||||
# Auth0 Configuration
|
||||
# ==========================================
|
||||
# Get these from your Auth0 dashboard:
|
||||
# 1. Go to https://manage.auth0.com/
|
||||
# 2. Create or select your Application (Single Page Application)
|
||||
# 3. Create or select your API
|
||||
# 4. Copy the values below
|
||||
|
||||
# Your Auth0 tenant domain (e.g., your-tenant.us.auth0.com)
|
||||
AUTH0_DOMAIN=your-tenant.us.auth0.com
|
||||
|
||||
# Your Auth0 API audience/identifier (e.g., https://vip-coordinator-api)
|
||||
AUTH0_AUDIENCE=https://your-api-identifier
|
||||
|
||||
# Your Auth0 issuer URL (usually https://your-tenant.us.auth0.com/)
|
||||
AUTH0_ISSUER=https://your-tenant.us.auth0.com/
|
||||
|
||||
# Your Auth0 SPA Client ID (this is public, used in frontend)
|
||||
AUTH0_CLIENT_ID=your-auth0-client-id
|
||||
|
||||
# ==========================================
|
||||
# Frontend Configuration
|
||||
# ==========================================
|
||||
# Port to expose the frontend on (default: 80)
|
||||
FRONTEND_PORT=80
|
||||
|
||||
# API URL for frontend to use (default: http://localhost/api/v1)
|
||||
# For production, this should be your domain's API endpoint
|
||||
# Note: In containerized setup, /api is proxied by nginx to backend
|
||||
VITE_API_URL=http://localhost/api/v1
|
||||
|
||||
# ==========================================
|
||||
# Optional: External APIs
|
||||
# ==========================================
|
||||
# AviationStack API key for flight tracking (optional)
|
||||
# Get one at: https://aviationstack.com/
|
||||
AVIATIONSTACK_API_KEY=
|
||||
|
||||
# ==========================================
|
||||
# Optional: Database Seeding
|
||||
# ==========================================
|
||||
# Set to 'true' to seed database with sample data on first run
|
||||
# WARNING: Only use in development/testing environments
|
||||
RUN_SEED=false
|
||||
|
||||
# ==========================================
|
||||
# Production Deployment Notes
|
||||
# ==========================================
|
||||
# 1. Configure Auth0:
|
||||
# - Add callback URLs: https://your-domain.com/callback
|
||||
# - Add allowed web origins: https://your-domain.com
|
||||
# - Add allowed logout URLs: https://your-domain.com
|
||||
#
|
||||
# 2. For HTTPS/SSL:
|
||||
# - Use a reverse proxy like Caddy, Traefik, or nginx-proxy
|
||||
# - Or configure cloud provider's load balancer with SSL certificate
|
||||
#
|
||||
# 3. First deployment:
|
||||
# docker-compose -f docker-compose.prod.yml up -d
|
||||
#
|
||||
# 4. To update:
|
||||
# docker-compose -f docker-compose.prod.yml down
|
||||
# docker-compose -f docker-compose.prod.yml build
|
||||
# docker-compose -f docker-compose.prod.yml up -d
|
||||
#
|
||||
# 5. View logs:
|
||||
# docker-compose -f docker-compose.prod.yml logs -f
|
||||
#
|
||||
# 6. Database migrations run automatically on backend startup
|
||||
96
.gitignore
vendored
96
.gitignore
vendored
@@ -1,27 +1,97 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
# Environment files with sensitive data
|
||||
.env.prod
|
||||
.env.production
|
||||
backend/.env
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
*.map
|
||||
# Node modules
|
||||
node_modules/
|
||||
backend/node_modules/
|
||||
frontend/node_modules/
|
||||
|
||||
# Build outputs
|
||||
backend/dist/
|
||||
frontend/dist/
|
||||
frontend/build/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
.claude/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
# AI context files
|
||||
CLAUDE.md
|
||||
|
||||
# CI/CD (GitHub-specific, not needed for Gitea)
|
||||
.github/
|
||||
|
||||
# E2E tests (keep locally for development, don't commit)
|
||||
frontend/e2e/
|
||||
**/playwright-report/
|
||||
**/test-results/
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Backup directories (exclude from repo)
|
||||
vip-coordinator-backup-*/
|
||||
# Backup files
|
||||
*backup*
|
||||
*.bak
|
||||
*.tmp
|
||||
*-old-*
|
||||
backend-old*
|
||||
frontend-old*
|
||||
|
||||
# ZIP files (exclude from repo)
|
||||
*.zip
|
||||
# Database files
|
||||
*.sqlite
|
||||
*.db
|
||||
|
||||
# Note: .env files are intentionally included in the repository
|
||||
# Redis dump
|
||||
dump.rdb
|
||||
@@ -1,174 +0,0 @@
|
||||
# ✅ CORRECTED Google OAuth Setup Guide
|
||||
|
||||
## ⚠️ Issues Found with Previous Setup
|
||||
|
||||
The previous coder was using **deprecated Google+ API** which was shut down in 2019. This guide provides the correct modern approach using Google Identity API.
|
||||
|
||||
## 🔧 What Was Fixed
|
||||
|
||||
1. **Removed Google+ API references** - Now uses Google Identity API
|
||||
2. **Fixed redirect URI configuration** - Points to backend instead of frontend
|
||||
3. **Added missing `/auth/setup` endpoint** - Frontend was calling non-existent endpoint
|
||||
4. **Corrected OAuth flow** - Proper backend callback handling
|
||||
|
||||
## 🚀 Correct Setup Instructions
|
||||
|
||||
### Step 1: Google Cloud Console Setup
|
||||
|
||||
1. **Go to Google Cloud Console**
|
||||
- Visit: https://console.cloud.google.com/
|
||||
|
||||
2. **Create or Select Project**
|
||||
- Create new project: "VIP Coordinator"
|
||||
- Or select existing project
|
||||
|
||||
3. **Enable Google Identity API** ⚠️ **NOT Google+ API**
|
||||
- Go to "APIs & Services" → "Library"
|
||||
- Search for "Google Identity API" or "Google+ API"
|
||||
- **Important**: Use "Google Identity API" - Google+ is deprecated!
|
||||
- Click "Enable"
|
||||
|
||||
4. **Create OAuth 2.0 Credentials**
|
||||
- Go to "APIs & Services" → "Credentials"
|
||||
- Click "Create Credentials" → "OAuth 2.0 Client IDs"
|
||||
- Application type: "Web application"
|
||||
- Name: "VIP Coordinator Web App"
|
||||
|
||||
5. **Configure Authorized URLs** ⚠️ **CRITICAL: Use Backend URLs**
|
||||
|
||||
**Authorized JavaScript origins:**
|
||||
```
|
||||
http://localhost:3000
|
||||
http://bsa.madeamess.online:3000
|
||||
```
|
||||
|
||||
**Authorized redirect URIs:** ⚠️ **Backend callback, NOT frontend**
|
||||
```
|
||||
http://localhost:3000/auth/google/callback
|
||||
http://bsa.madeamess.online:3000/auth/google/callback
|
||||
```
|
||||
|
||||
6. **Save Credentials**
|
||||
- Copy **Client ID** and **Client Secret**
|
||||
|
||||
### Step 2: Update Environment Variables
|
||||
|
||||
Edit `backend/.env`:
|
||||
|
||||
```bash
|
||||
# Replace these values with your actual Google OAuth credentials
|
||||
GOOGLE_CLIENT_ID=your-actual-client-id-here.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=GOCSPX-your-actual-client-secret-here
|
||||
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
|
||||
|
||||
# For production, also update:
|
||||
# GOOGLE_REDIRECT_URI=http://bsa.madeamess.online:3000/auth/google/callback
|
||||
```
|
||||
|
||||
### Step 3: Test the Setup
|
||||
|
||||
1. **Restart the backend:**
|
||||
```bash
|
||||
cd vip-coordinator
|
||||
docker-compose -f docker-compose.dev.yml restart backend
|
||||
```
|
||||
|
||||
2. **Test the OAuth flow:**
|
||||
- Visit: http://localhost:5173 (or your frontend URL)
|
||||
- Click "Continue with Google"
|
||||
- Should redirect to Google login
|
||||
- After login, should redirect back and log you in
|
||||
|
||||
3. **Check backend logs:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml logs backend
|
||||
```
|
||||
|
||||
## 🔍 How the Corrected Flow Works
|
||||
|
||||
1. **User clicks "Continue with Google"**
|
||||
2. **Frontend calls** `/auth/google/url` to get OAuth URL
|
||||
3. **Frontend redirects** to Google OAuth
|
||||
4. **Google redirects back** to `http://localhost:3000/auth/google/callback`
|
||||
5. **Backend handles callback**, exchanges code for user info
|
||||
6. **Backend creates JWT token** and redirects to frontend with token
|
||||
7. **Frontend receives token** and authenticates user
|
||||
|
||||
## 🛠️ Key Differences from Previous Implementation
|
||||
|
||||
| Previous (Broken) | Corrected |
|
||||
|-------------------|-----------|
|
||||
| Google+ API (deprecated) | Google Identity API |
|
||||
| Frontend redirect URI | Backend redirect URI |
|
||||
| Missing `/auth/setup` endpoint | Added setup status endpoint |
|
||||
| Inconsistent OAuth flow | Standard OAuth 2.0 flow |
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues:
|
||||
|
||||
1. **"OAuth not configured" error:**
|
||||
- Check `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in `.env`
|
||||
- Restart backend after changing environment variables
|
||||
|
||||
2. **"Invalid redirect URI" error:**
|
||||
- Verify redirect URIs in Google Console match exactly:
|
||||
- `http://localhost:3000/auth/google/callback`
|
||||
- `http://bsa.madeamess.online:3000/auth/google/callback`
|
||||
- No trailing slashes!
|
||||
|
||||
3. **"API not enabled" error:**
|
||||
- Make sure you enabled "Google Identity API" (not Google+)
|
||||
- Wait a few minutes for API to activate
|
||||
|
||||
4. **Login button doesn't work:**
|
||||
- Check browser console for errors
|
||||
- Verify backend is running on port 3000
|
||||
- Check `/auth/setup` endpoint returns proper status
|
||||
|
||||
### Debug Commands:
|
||||
|
||||
```bash
|
||||
# Check if backend is running
|
||||
curl http://localhost:3000/api/health
|
||||
|
||||
# Check OAuth setup status
|
||||
curl http://localhost:3000/auth/setup
|
||||
|
||||
# Check backend logs
|
||||
docker-compose -f docker-compose.dev.yml logs backend
|
||||
|
||||
# Check environment variables are loaded
|
||||
docker exec vip-coordinator-backend-1 env | grep GOOGLE
|
||||
```
|
||||
|
||||
## ✅ Verification Steps
|
||||
|
||||
1. **Setup status should show configured:**
|
||||
```bash
|
||||
curl http://localhost:3000/auth/setup
|
||||
# Should return: {"setupCompleted": true, "firstAdminCreated": false, "oauthConfigured": true}
|
||||
```
|
||||
|
||||
2. **OAuth URL should be generated:**
|
||||
```bash
|
||||
curl http://localhost:3000/auth/google/url
|
||||
# Should return: {"url": "https://accounts.google.com/o/oauth2/v2/auth?..."}
|
||||
```
|
||||
|
||||
3. **Login flow should work:**
|
||||
- Visit frontend
|
||||
- Click "Continue with Google"
|
||||
- Complete Google login
|
||||
- Should be redirected back and logged in
|
||||
|
||||
## 🎉 Success!
|
||||
|
||||
Once working, you should see:
|
||||
- ✅ Google login button works
|
||||
- ✅ Redirects to Google OAuth
|
||||
- ✅ Returns to app after login
|
||||
- ✅ User is authenticated with JWT token
|
||||
- ✅ First user becomes administrator
|
||||
|
||||
The authentication system now uses modern Google Identity API and follows proper OAuth 2.0 standards!
|
||||
@@ -1,221 +0,0 @@
|
||||
# VIP Coordinator Database Migration Summary
|
||||
|
||||
## Overview
|
||||
Successfully migrated the VIP Coordinator application from JSON file storage to a proper database architecture using PostgreSQL and Redis.
|
||||
|
||||
## Architecture Changes
|
||||
|
||||
### Before (JSON File Storage)
|
||||
- All data stored in `backend/data/vip-coordinator.json`
|
||||
- Single file for VIPs, drivers, schedules, and admin settings
|
||||
- No concurrent access control
|
||||
- No real-time capabilities
|
||||
- Risk of data corruption
|
||||
|
||||
### After (PostgreSQL + Redis)
|
||||
- **PostgreSQL**: Persistent business data with ACID compliance
|
||||
- **Redis**: Real-time data and caching
|
||||
- Proper data relationships and constraints
|
||||
- Concurrent access support
|
||||
- Real-time location tracking
|
||||
- Flight data caching
|
||||
|
||||
## Database Schema
|
||||
|
||||
### PostgreSQL Tables
|
||||
1. **vips** - VIP profiles and basic information
|
||||
2. **flights** - Flight details linked to VIPs
|
||||
3. **drivers** - Driver profiles
|
||||
4. **schedule_events** - Event scheduling with driver assignments
|
||||
5. **admin_settings** - System configuration (key-value pairs)
|
||||
|
||||
### Redis Data Structure
|
||||
- `driver:{id}:location` - Real-time driver locations
|
||||
- `event:{id}:status` - Live event status updates
|
||||
- `flight:{key}` - Cached flight API responses
|
||||
|
||||
## Key Features Implemented
|
||||
|
||||
### 1. Database Configuration
|
||||
- **PostgreSQL connection pool** (`backend/src/config/database.ts`)
|
||||
- **Redis client setup** (`backend/src/config/redis.ts`)
|
||||
- **Database schema** (`backend/src/config/schema.sql`)
|
||||
|
||||
### 2. Data Services
|
||||
- **DatabaseService** (`backend/src/services/databaseService.ts`)
|
||||
- Database initialization and migration
|
||||
- Redis operations for real-time data
|
||||
- Automatic JSON data migration
|
||||
- **EnhancedDataService** (`backend/src/services/enhancedDataService.ts`)
|
||||
- PostgreSQL CRUD operations
|
||||
- Complex queries with joins
|
||||
- Transaction support
|
||||
|
||||
### 3. Migration Features
|
||||
- **Automatic migration** from existing JSON data
|
||||
- **Backup creation** of original JSON file
|
||||
- **Zero-downtime migration** process
|
||||
- **Data validation** during migration
|
||||
|
||||
### 4. Real-time Capabilities
|
||||
- **Driver location tracking** in Redis
|
||||
- **Event status updates** with timestamps
|
||||
- **Flight data caching** with TTL
|
||||
- **Performance optimization** through caching
|
||||
|
||||
## Data Flow
|
||||
|
||||
### VIP Management
|
||||
```
|
||||
Frontend → API → EnhancedDataService → PostgreSQL
|
||||
→ Redis (for real-time data)
|
||||
```
|
||||
|
||||
### Driver Location Updates
|
||||
```
|
||||
Frontend → API → DatabaseService → Redis (hSet driver location)
|
||||
```
|
||||
|
||||
### Flight Tracking
|
||||
```
|
||||
Flight API → FlightService → Redis (cache) → Database (if needed)
|
||||
```
|
||||
|
||||
## Benefits Achieved
|
||||
|
||||
### Performance
|
||||
- **Faster queries** with PostgreSQL indexes
|
||||
- **Reduced API calls** through Redis caching
|
||||
- **Concurrent access** without file locking issues
|
||||
|
||||
### Scalability
|
||||
- **Multiple server instances** supported
|
||||
- **Database connection pooling**
|
||||
- **Redis clustering** ready
|
||||
|
||||
### Reliability
|
||||
- **ACID transactions** for data integrity
|
||||
- **Automatic backups** during migration
|
||||
- **Error handling** and rollback support
|
||||
|
||||
### Real-time Features
|
||||
- **Live driver locations** via Redis
|
||||
- **Event status tracking** with timestamps
|
||||
- **Flight data caching** for performance
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
DATABASE_URL=postgresql://postgres:changeme@db:5432/vip_coordinator
|
||||
REDIS_URL=redis://redis:6379
|
||||
```
|
||||
|
||||
### Docker Services
|
||||
- **PostgreSQL 15** with persistent volume
|
||||
- **Redis 7** for caching and real-time data
|
||||
- **Backend** with database connections
|
||||
|
||||
## Migration Process
|
||||
|
||||
### Automatic Steps
|
||||
1. **Schema creation** with tables and indexes
|
||||
2. **Data validation** and transformation
|
||||
3. **VIP migration** with flight relationships
|
||||
4. **Driver migration** with location data to Redis
|
||||
5. **Schedule migration** with proper relationships
|
||||
6. **Admin settings** flattened to key-value pairs
|
||||
7. **Backup creation** of original JSON file
|
||||
|
||||
### Manual Steps (if needed)
|
||||
1. Install dependencies: `npm install`
|
||||
2. Start services: `make dev`
|
||||
3. Verify migration in logs
|
||||
|
||||
## API Changes
|
||||
|
||||
### Enhanced Endpoints
|
||||
- All VIP endpoints now use PostgreSQL
|
||||
- Driver location updates go to Redis
|
||||
- Flight data cached in Redis
|
||||
- Schedule operations with proper relationships
|
||||
|
||||
### Backward Compatibility
|
||||
- All existing API endpoints maintained
|
||||
- Same request/response formats
|
||||
- Legacy field support during transition
|
||||
|
||||
## Testing
|
||||
|
||||
### Database Connection
|
||||
```bash
|
||||
# Health check includes database status
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
### Data Verification
|
||||
```bash
|
||||
# Check VIPs migrated correctly
|
||||
curl http://localhost:3000/api/vips
|
||||
|
||||
# Check drivers with locations
|
||||
curl http://localhost:3000/api/drivers
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate
|
||||
1. **Test the migration** with Docker
|
||||
2. **Verify all endpoints** work correctly
|
||||
3. **Check real-time features** function
|
||||
|
||||
### Future Enhancements
|
||||
1. **WebSocket integration** for live updates
|
||||
2. **Advanced Redis patterns** for pub/sub
|
||||
3. **Database optimization** with query analysis
|
||||
4. **Monitoring and metrics** setup
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
### New Files
|
||||
- `backend/src/config/database.ts` - PostgreSQL configuration
|
||||
- `backend/src/config/redis.ts` - Redis configuration
|
||||
- `backend/src/config/schema.sql` - Database schema
|
||||
- `backend/src/services/databaseService.ts` - Migration and Redis ops
|
||||
- `backend/src/services/enhancedDataService.ts` - PostgreSQL operations
|
||||
|
||||
### Modified Files
|
||||
- `backend/package.json` - Added pg, redis, uuid dependencies
|
||||
- `backend/src/index.ts` - Updated to use new services
|
||||
- `docker-compose.dev.yml` - Already configured for databases
|
||||
|
||||
## Redis Usage Patterns
|
||||
|
||||
### Driver Locations
|
||||
```typescript
|
||||
// Update location
|
||||
await databaseService.updateDriverLocation(driverId, { lat: 39.7392, lng: -104.9903 });
|
||||
|
||||
// Get location
|
||||
const location = await databaseService.getDriverLocation(driverId);
|
||||
```
|
||||
|
||||
### Event Status
|
||||
```typescript
|
||||
// Set status
|
||||
await databaseService.setEventStatus(eventId, 'in-progress');
|
||||
|
||||
// Get status
|
||||
const status = await databaseService.getEventStatus(eventId);
|
||||
```
|
||||
|
||||
### Flight Caching
|
||||
```typescript
|
||||
// Cache flight data
|
||||
await databaseService.cacheFlightData(flightKey, flightData, 300);
|
||||
|
||||
// Get cached data
|
||||
const cached = await databaseService.getCachedFlightData(flightKey);
|
||||
```
|
||||
|
||||
This migration provides a solid foundation for scaling the VIP Coordinator application with proper data persistence, real-time capabilities, and performance optimization.
|
||||
@@ -1,179 +0,0 @@
|
||||
# Docker Container Stopping Issues - Troubleshooting Guide
|
||||
|
||||
## 🚨 Issue Observed
|
||||
|
||||
During development, we encountered issues where Docker containers would hang during the stopping process, requiring forceful termination. This is concerning for production stability.
|
||||
|
||||
## 🔍 Current System Status
|
||||
|
||||
**✅ All containers are currently running properly:**
|
||||
- Backend: http://localhost:3000 (responding correctly)
|
||||
- Frontend: http://localhost:5173
|
||||
- Database: PostgreSQL on port 5432
|
||||
- Redis: Running on port 6379
|
||||
|
||||
**Docker Configuration:**
|
||||
- Storage Driver: overlay2
|
||||
- Logging Driver: json-file
|
||||
- Cgroup Driver: systemd
|
||||
- Cgroup Version: 2
|
||||
|
||||
## 🛠️ Potential Causes & Solutions
|
||||
|
||||
### 1. **Graceful Shutdown Issues**
|
||||
**Problem:** Applications not handling SIGTERM signals properly
|
||||
**Solution:** Ensure applications handle shutdown gracefully
|
||||
|
||||
**For Node.js apps (backend/frontend):**
|
||||
```javascript
|
||||
// Add to your main application file
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
console.log('Process terminated');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
console.log('Process terminated');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. **Docker Compose Configuration**
|
||||
**Current issue:** Using obsolete `version` attribute
|
||||
**Solution:** Update docker-compose.dev.yml
|
||||
|
||||
```yaml
|
||||
# Remove this line:
|
||||
# version: '3.8'
|
||||
|
||||
# And ensure proper stop configuration:
|
||||
services:
|
||||
backend:
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
|
||||
frontend:
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
```
|
||||
|
||||
### 3. **Resource Constraints**
|
||||
**Problem:** Insufficient memory/CPU causing hanging
|
||||
**Solution:** Add resource limits
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
reservations:
|
||||
memory: 256M
|
||||
```
|
||||
|
||||
### 4. **Database Connection Handling**
|
||||
**Problem:** Open database connections preventing shutdown
|
||||
**Solution:** Ensure proper connection cleanup
|
||||
|
||||
```javascript
|
||||
// In your backend application
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('Closing database connections...');
|
||||
await database.close();
|
||||
await redis.quit();
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
## 🔧 Immediate Fixes to Implement
|
||||
|
||||
### 1. Update Docker Compose File
|
||||
```bash
|
||||
cd /home/kyle/Desktop/vip-coordinator
|
||||
# Remove the version line and add stop configurations
|
||||
```
|
||||
|
||||
### 2. Add Graceful Shutdown to Backend
|
||||
```bash
|
||||
# Update backend/src/index.ts with proper signal handling
|
||||
```
|
||||
|
||||
### 3. Monitor Container Behavior
|
||||
```bash
|
||||
# Use these commands to monitor:
|
||||
docker-compose -f docker-compose.dev.yml logs --follow
|
||||
docker stats
|
||||
```
|
||||
|
||||
## 🚨 Emergency Commands
|
||||
|
||||
If containers hang during stopping:
|
||||
|
||||
```bash
|
||||
# Force stop all containers
|
||||
docker-compose -f docker-compose.dev.yml kill
|
||||
|
||||
# Remove stopped containers
|
||||
docker-compose -f docker-compose.dev.yml rm -f
|
||||
|
||||
# Clean up system
|
||||
docker system prune -f
|
||||
|
||||
# Restart fresh
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
## 📊 Monitoring Commands
|
||||
|
||||
```bash
|
||||
# Check container status
|
||||
docker-compose -f docker-compose.dev.yml ps
|
||||
|
||||
# Monitor logs in real-time
|
||||
docker-compose -f docker-compose.dev.yml logs -f backend
|
||||
|
||||
# Check resource usage
|
||||
docker stats
|
||||
|
||||
# Check for hanging processes
|
||||
docker-compose -f docker-compose.dev.yml top
|
||||
```
|
||||
|
||||
## 🎯 Prevention Strategies
|
||||
|
||||
1. **Regular Health Checks**
|
||||
- Implement health check endpoints
|
||||
- Monitor container resource usage
|
||||
- Set up automated restarts for failed containers
|
||||
|
||||
2. **Proper Signal Handling**
|
||||
- Ensure all applications handle SIGTERM/SIGINT
|
||||
- Implement graceful shutdown procedures
|
||||
- Close database connections properly
|
||||
|
||||
3. **Resource Management**
|
||||
- Set appropriate memory/CPU limits
|
||||
- Monitor disk space usage
|
||||
- Regular cleanup of unused images/containers
|
||||
|
||||
## 🔄 Current OAuth2 Status
|
||||
|
||||
**✅ OAuth2 is now working correctly:**
|
||||
- Simplified implementation without Passport.js
|
||||
- Proper domain configuration for bsa.madeamess.online
|
||||
- Environment variables correctly set
|
||||
- Backend responding to auth endpoints
|
||||
|
||||
**Next steps for OAuth2:**
|
||||
1. Update Google Cloud Console with redirect URI: `https://bsa.madeamess.online:3000/auth/google/callback`
|
||||
2. Test the full OAuth flow
|
||||
3. Integrate with frontend
|
||||
|
||||
The container stopping issues are separate from the OAuth2 functionality and should be addressed through the solutions above.
|
||||
@@ -1,173 +0,0 @@
|
||||
# VIP Coordinator Documentation Cleanup - COMPLETED ✅
|
||||
|
||||
## 🎉 Complete Documentation Cleanup Successfully Finished
|
||||
|
||||
The VIP Coordinator project has been **completely cleaned up and modernized**. We've streamlined from **30+ files** down to **10 essential files**, removing all outdated documentation and redundant scripts.
|
||||
|
||||
## 📊 Final Results
|
||||
|
||||
### Before Cleanup (30+ files)
|
||||
- **9 OAuth setup guides** - Multiple confusing, outdated approaches
|
||||
- **8 Test data scripts** - External scripts for data population
|
||||
- **3 One-time utility scripts** - API testing and migration scripts
|
||||
- **8 Redundant documentation** - User management, troubleshooting, RBAC docs
|
||||
- **2 Database migration docs** - Completed migration summaries
|
||||
- **Scattered information** across many files
|
||||
|
||||
### After Cleanup (10 files)
|
||||
- **1 Setup guide** - Single, comprehensive SETUP_GUIDE.md
|
||||
- **1 Project overview** - Modern README.md with current features
|
||||
- **1 API guide** - Detailed README-API.md
|
||||
- **2 API documentation files** - Interactive Swagger UI and OpenAPI spec
|
||||
- **3 Docker configuration files** - Development and production environments
|
||||
- **1 Development tool** - Makefile for commands
|
||||
- **2 Code directories** - backend/ and frontend/
|
||||
|
||||
## ✅ Total Files Removed: 28 files
|
||||
|
||||
### OAuth Documentation (9 files) ❌ REMOVED
|
||||
- CORRECTED_GOOGLE_OAUTH_SETUP.md
|
||||
- GOOGLE_OAUTH_DOMAIN_SETUP.md
|
||||
- GOOGLE_OAUTH_QUICK_SETUP.md
|
||||
- GOOGLE_OAUTH_SETUP.md
|
||||
- OAUTH_CALLBACK_FIX_SUMMARY.md
|
||||
- OAUTH_FRONTEND_ONLY_SETUP.md
|
||||
- REVERSE_PROXY_OAUTH_SETUP.md
|
||||
- SIMPLE_OAUTH_SETUP.md
|
||||
- WEB_SERVER_PROXY_SETUP.md
|
||||
|
||||
### Test Data Scripts (8 files) ❌ REMOVED
|
||||
*Reason: Built into admin dashboard UI*
|
||||
- populate-events-dynamic.js
|
||||
- populate-events-dynamic.sh
|
||||
- populate-events.js
|
||||
- populate-events.sh
|
||||
- populate-test-data.js
|
||||
- populate-test-data.sh
|
||||
- populate-vips.js
|
||||
- quick-populate-events.sh
|
||||
|
||||
### One-Time Utility Scripts (3 files) ❌ REMOVED
|
||||
*Reason: No longer needed*
|
||||
- test-aviationstack-endpoints.js (hardcoded API key, one-time testing)
|
||||
- test-flight-api.js (redundant with admin dashboard API testing)
|
||||
- update-departments.js (one-time migration script, already run)
|
||||
|
||||
### Redundant Documentation (8 files) ❌ REMOVED
|
||||
- DATABASE_MIGRATION_SUMMARY.md
|
||||
- POSTGRESQL_USER_MANAGEMENT.md
|
||||
- SIMPLE_USER_MANAGEMENT.md
|
||||
- USER_MANAGEMENT_RECOMMENDATIONS.md
|
||||
- DOCKER_TROUBLESHOOTING.md
|
||||
- PERMISSION_ISSUES_FIXED.md
|
||||
- PORT_3000_SETUP_GUIDE.md
|
||||
- ROLE_BASED_ACCESS_CONTROL.md
|
||||
|
||||
## 📚 Essential Files Preserved (10 files)
|
||||
|
||||
### Core Documentation ✅
|
||||
1. **README.md** - Modern project overview with current features
|
||||
2. **SETUP_GUIDE.md** - Comprehensive setup guide with Google OAuth
|
||||
3. **README-API.md** - Detailed API documentation and examples
|
||||
|
||||
### API Documentation ✅
|
||||
4. **api-docs.html** - Interactive Swagger UI documentation
|
||||
5. **api-documentation.yaml** - OpenAPI specification
|
||||
|
||||
### Development Configuration ✅
|
||||
6. **Makefile** - Development commands and workflows
|
||||
7. **docker-compose.dev.yml** - Development environment
|
||||
8. **docker-compose.prod.yml** - Production environment
|
||||
|
||||
### Project Structure ✅
|
||||
9. **backend/** - Complete Node.js API server
|
||||
10. **frontend/** - Complete React application
|
||||
|
||||
## 🚀 Key Improvements Achieved
|
||||
|
||||
### 1. **Simplified Setup Process**
|
||||
- **Before**: 9+ OAuth guides with conflicting instructions
|
||||
- **After**: Single SETUP_GUIDE.md with clear, step-by-step Google OAuth setup
|
||||
|
||||
### 2. **Modernized Test Data Management**
|
||||
- **Before**: 8 external scripts requiring manual execution
|
||||
- **After**: Built-in admin dashboard with one-click test data creation/removal
|
||||
|
||||
### 3. **Streamlined Documentation Maintenance**
|
||||
- **Before**: 28+ files to keep updated
|
||||
- **After**: 3 core documentation files (90% reduction in maintenance)
|
||||
|
||||
### 4. **Accurate System Representation**
|
||||
- **Before**: Outdated documentation scattered across many files
|
||||
- **After**: Current documentation reflecting JWT + Google OAuth architecture
|
||||
|
||||
### 5. **Clean Project Structure**
|
||||
- **Before**: Root directory cluttered with 30+ files
|
||||
- **After**: Clean, organized structure with only essential files
|
||||
|
||||
## 🎯 Current System (Properly Documented)
|
||||
|
||||
### Authentication System ✅
|
||||
- **JWT-based authentication** with Google OAuth
|
||||
- **Role-based access control**: Administrator, Coordinator, Driver
|
||||
- **User approval system** for new registrations
|
||||
- **Simple setup** documented in SETUP_GUIDE.md
|
||||
|
||||
### Test Data Management ✅
|
||||
- **Built-in admin dashboard** for test data creation
|
||||
- **One-click VIP generation** (20 diverse test VIPs with full schedules)
|
||||
- **Easy cleanup** - remove all test data with one click
|
||||
- **No external scripts needed**
|
||||
|
||||
### API Documentation ✅
|
||||
- **Interactive Swagger UI** at `/api-docs.html`
|
||||
- **"Try it out" functionality** for testing endpoints
|
||||
- **Comprehensive API guide** in README-API.md
|
||||
|
||||
### Development Workflow ✅
|
||||
- **Single command setup**: `make dev`
|
||||
- **Docker-based development** with automatic database initialization
|
||||
- **Clear troubleshooting** in SETUP_GUIDE.md
|
||||
|
||||
## 📋 Developer Experience
|
||||
|
||||
### New Developer Onboarding
|
||||
1. **Clone repository**
|
||||
2. **Follow SETUP_GUIDE.md** (single source of truth)
|
||||
3. **Run `make dev`** (starts everything)
|
||||
4. **Configure Google OAuth** (clear instructions)
|
||||
5. **Use admin dashboard** for test data (no scripts)
|
||||
6. **Access API docs** at localhost:3000/api-docs.html
|
||||
|
||||
### Documentation Maintenance
|
||||
- **3 files to maintain** (vs. 28+ before)
|
||||
- **No redundant information**
|
||||
- **Clear ownership** of each documentation area
|
||||
|
||||
## 🎉 Success Metrics
|
||||
|
||||
- ✅ **28 files removed** (74% reduction)
|
||||
- ✅ **All essential functionality preserved**
|
||||
- ✅ **Test data management modernized**
|
||||
- ✅ **Single, clear setup path established**
|
||||
- ✅ **Documentation reflects current architecture**
|
||||
- ✅ **Dramatically improved developer experience**
|
||||
- ✅ **Massive reduction in maintenance burden**
|
||||
|
||||
## 🔮 Future Maintenance
|
||||
|
||||
### What to Keep Updated
|
||||
1. **README.md** - Project overview and features
|
||||
2. **SETUP_GUIDE.md** - Setup instructions and troubleshooting
|
||||
3. **README-API.md** - API documentation and examples
|
||||
|
||||
### What's Self-Maintaining
|
||||
- **api-docs.html** - Generated from OpenAPI spec
|
||||
- **Test data** - Built into admin dashboard
|
||||
- **OAuth setup** - Simplified to basic Google OAuth
|
||||
|
||||
---
|
||||
|
||||
**The VIP Coordinator project now has clean, current, and maintainable documentation that accurately reflects the modern system architecture!** 🚀
|
||||
|
||||
**Total Impact**: From 30+ files to 10 essential files (74% reduction) while significantly improving functionality and developer experience.
|
||||
@@ -1,108 +0,0 @@
|
||||
# Google OAuth2 Domain Setup for bsa.madeamess.online
|
||||
|
||||
## 🔧 Current Configuration
|
||||
|
||||
Your VIP Coordinator is now configured for your domain:
|
||||
- **Backend URL**: `https://bsa.madeamess.online:3000`
|
||||
- **Frontend URL**: `https://bsa.madeamess.online:5173`
|
||||
- **OAuth Redirect URI**: `https://bsa.madeamess.online:3000/auth/google/callback`
|
||||
|
||||
## 📋 Google Cloud Console Setup
|
||||
|
||||
You need to update your Google Cloud Console OAuth2 configuration:
|
||||
|
||||
### 1. Go to Google Cloud Console
|
||||
- Visit: https://console.cloud.google.com/
|
||||
- Select your project (or create one)
|
||||
|
||||
### 2. Enable APIs
|
||||
- Go to "APIs & Services" → "Library"
|
||||
- Enable "Google+ API" (or "People API")
|
||||
|
||||
### 3. Configure OAuth2 Credentials
|
||||
- Go to "APIs & Services" → "Credentials"
|
||||
- Find your OAuth 2.0 Client ID: `308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com`
|
||||
- Click "Edit" (pencil icon)
|
||||
|
||||
### 4. Update Authorized Redirect URIs
|
||||
Add these exact URIs (case-sensitive):
|
||||
```
|
||||
https://bsa.madeamess.online:3000/auth/google/callback
|
||||
```
|
||||
|
||||
### 5. Update Authorized JavaScript Origins (if needed)
|
||||
Add these origins:
|
||||
```
|
||||
https://bsa.madeamess.online:3000
|
||||
https://bsa.madeamess.online:5173
|
||||
```
|
||||
|
||||
## 🚀 Testing the OAuth Flow
|
||||
|
||||
Once you've updated Google Cloud Console:
|
||||
|
||||
1. **Visit the OAuth endpoint:**
|
||||
```
|
||||
https://bsa.madeamess.online:3000/auth/google
|
||||
```
|
||||
|
||||
2. **Expected flow:**
|
||||
- Redirects to Google login
|
||||
- After login, Google redirects to: `https://bsa.madeamess.online:3000/auth/google/callback`
|
||||
- Backend processes the callback and redirects to: `https://bsa.madeamess.online:5173/auth/callback?token=JWT_TOKEN`
|
||||
|
||||
3. **Check if backend is running:**
|
||||
```bash
|
||||
curl https://bsa.madeamess.online:3000/api/health
|
||||
```
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Common Issues:
|
||||
|
||||
1. **"redirect_uri_mismatch" error:**
|
||||
- Make sure the redirect URI in Google Console exactly matches: `https://bsa.madeamess.online:3000/auth/google/callback`
|
||||
- No trailing slashes
|
||||
- Exact case match
|
||||
- Include the port number `:3000`
|
||||
|
||||
2. **SSL/HTTPS issues:**
|
||||
- Make sure your domain has valid SSL certificates
|
||||
- Google requires HTTPS for production OAuth
|
||||
|
||||
3. **Port access:**
|
||||
- Ensure ports 3000 and 5173 are accessible from the internet
|
||||
- Check firewall settings
|
||||
|
||||
### Debug Commands:
|
||||
```bash
|
||||
# Check if containers are running
|
||||
docker-compose -f docker-compose.dev.yml ps
|
||||
|
||||
# Check backend logs
|
||||
docker-compose -f docker-compose.dev.yml logs backend
|
||||
|
||||
# Test backend health
|
||||
curl https://bsa.madeamess.online:3000/api/health
|
||||
|
||||
# Test auth status
|
||||
curl https://bsa.madeamess.online:3000/auth/status
|
||||
```
|
||||
|
||||
## 📝 Current Environment Variables
|
||||
|
||||
Your `.env` file is configured with:
|
||||
```bash
|
||||
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
GOOGLE_REDIRECT_URI=https://bsa.madeamess.online:3000/auth/google/callback
|
||||
FRONTEND_URL=https://bsa.madeamess.online:5173
|
||||
```
|
||||
|
||||
## ✅ Next Steps
|
||||
|
||||
1. Update Google Cloud Console with the redirect URI above
|
||||
2. Test the OAuth flow by visiting `https://bsa.madeamess.online:3000/auth/google`
|
||||
3. Verify the frontend can handle the callback at `https://bsa.madeamess.online:5173/auth/callback`
|
||||
|
||||
The OAuth2 system should now work correctly with your domain! 🎉
|
||||
@@ -1,48 +0,0 @@
|
||||
# Quick Google OAuth Setup Guide
|
||||
|
||||
## Step 1: Get Your Google OAuth Credentials
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select an existing one
|
||||
3. Enable the Google+ API (or Google Identity API)
|
||||
4. Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client IDs"
|
||||
5. Set Application type to "Web application"
|
||||
6. Add these Authorized redirect URIs:
|
||||
- `http://localhost:5173/auth/google/callback`
|
||||
- `http://bsa.madeamess.online:5173/auth/google/callback`
|
||||
|
||||
## Step 2: Update Your .env File
|
||||
|
||||
Replace these lines in `/home/kyle/Desktop/vip-coordinator/backend/.env`:
|
||||
|
||||
```bash
|
||||
# REPLACE THESE TWO LINES:
|
||||
GOOGLE_CLIENT_ID=your-google-client-id-from-console
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret-from-console
|
||||
|
||||
# WITH YOUR ACTUAL VALUES:
|
||||
GOOGLE_CLIENT_ID=123456789-abcdefghijklmnop.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=GOCSPX-your_actual_secret_here
|
||||
```
|
||||
|
||||
## Step 3: Restart the Backend
|
||||
|
||||
After updating the .env file, restart the backend container:
|
||||
|
||||
```bash
|
||||
cd /home/kyle/Desktop/vip-coordinator
|
||||
docker-compose -f docker-compose.dev.yml restart backend
|
||||
```
|
||||
|
||||
## Step 4: Test the Login
|
||||
|
||||
Visit: http://bsa.madeamess.online:5173 and click "Sign in with Google"
|
||||
(The frontend proxies /auth requests to the backend automatically)
|
||||
|
||||
## Bypass Option (Temporary)
|
||||
|
||||
If you want to skip Google OAuth for now, visit:
|
||||
http://bsa.madeamess.online:5173/admin-bypass
|
||||
|
||||
This will take you directly to the admin dashboard without authentication.
|
||||
(The frontend will proxy this request to the backend)
|
||||
@@ -1,108 +0,0 @@
|
||||
# Google OAuth Setup Guide
|
||||
|
||||
## Overview
|
||||
Your VIP Coordinator now includes Google OAuth authentication! This guide will help you set up Google OAuth credentials so users can log in with their Google accounts.
|
||||
|
||||
## Step 1: Google Cloud Console Setup
|
||||
|
||||
### 1. Go to Google Cloud Console
|
||||
Visit: https://console.cloud.google.com/
|
||||
|
||||
### 2. Create or Select a Project
|
||||
- If you don't have a project, click "Create Project"
|
||||
- Give it a name like "VIP Coordinator"
|
||||
- Select your organization if applicable
|
||||
|
||||
### 3. Enable Google+ API
|
||||
- Go to "APIs & Services" → "Library"
|
||||
- Search for "Google+ API"
|
||||
- Click on it and press "Enable"
|
||||
|
||||
### 4. Create OAuth 2.0 Credentials
|
||||
- Go to "APIs & Services" → "Credentials"
|
||||
- Click "Create Credentials" → "OAuth 2.0 Client IDs"
|
||||
- Choose "Web application" as the application type
|
||||
- Give it a name like "VIP Coordinator Web App"
|
||||
|
||||
### 5. Configure Authorized URLs
|
||||
**Authorized JavaScript origins:**
|
||||
```
|
||||
http://bsa.madeamess.online:5173
|
||||
http://localhost:5173
|
||||
```
|
||||
|
||||
**Authorized redirect URIs:**
|
||||
```
|
||||
http://bsa.madeamess.online:3000/auth/google/callback
|
||||
http://localhost:3000/auth/google/callback
|
||||
```
|
||||
|
||||
### 6. Save Your Credentials
|
||||
- Copy the **Client ID** and **Client Secret**
|
||||
- You'll need these for the next step
|
||||
|
||||
## Step 2: Configure VIP Coordinator
|
||||
|
||||
### 1. Access Admin Dashboard
|
||||
- Go to: http://bsa.madeamess.online:5173/admin
|
||||
- Enter the admin password: `admin123`
|
||||
|
||||
### 2. Add Google OAuth Credentials
|
||||
- Scroll to the "Google OAuth Credentials" section
|
||||
- Paste your **Client ID** in the first field
|
||||
- Paste your **Client Secret** in the second field
|
||||
- Click "Save All Settings"
|
||||
|
||||
## Step 3: Test the Setup
|
||||
|
||||
### 1. Access the Application
|
||||
- Go to: http://bsa.madeamess.online:5173
|
||||
- You should see a Google login button
|
||||
|
||||
### 2. First Login (Admin Setup)
|
||||
- The first person to log in will automatically become the administrator
|
||||
- Subsequent users will be assigned the "coordinator" role by default
|
||||
- Drivers will need to register separately
|
||||
|
||||
### 3. User Roles
|
||||
- **Administrator**: Full system access, user management, settings
|
||||
- **Coordinator**: VIP and schedule management, driver assignments
|
||||
- **Driver**: Personal schedule view, location updates
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues:
|
||||
|
||||
1. **"Blocked request" error**
|
||||
- Make sure your domain is added to authorized JavaScript origins
|
||||
- Check that the redirect URI matches exactly
|
||||
|
||||
2. **"OAuth credentials not configured" warning**
|
||||
- Verify you've entered both Client ID and Client Secret
|
||||
- Make sure you clicked "Save All Settings"
|
||||
|
||||
3. **Login button not working**
|
||||
- Check browser console for errors
|
||||
- Verify the backend is running on port 3000
|
||||
|
||||
### Getting Help:
|
||||
- Check the browser console for error messages
|
||||
- Verify all URLs match exactly (including http/https)
|
||||
- Make sure the Google+ API is enabled in your project
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Keep your Client Secret secure and never share it publicly
|
||||
- The credentials are stored securely in your database
|
||||
- Sessions last 24 hours as requested
|
||||
- Only the frontend (port 5173) is exposed externally for security
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once Google OAuth is working:
|
||||
1. Test the login flow with different Google accounts
|
||||
2. Assign appropriate roles to users through the admin dashboard
|
||||
3. Create VIPs and schedules to test the full system
|
||||
4. Set up additional API keys (AviationStack, etc.) as needed
|
||||
|
||||
Your VIP Coordinator is now ready for secure, role-based access!
|
||||
10
Makefile
10
Makefile
@@ -1,10 +0,0 @@
|
||||
.PHONY: dev build deploy
|
||||
|
||||
dev:
|
||||
docker-compose -f docker-compose.dev.yml up --build
|
||||
|
||||
build:
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
|
||||
deploy:
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
@@ -1,92 +0,0 @@
|
||||
# ✅ OAuth Callback Issue RESOLVED!
|
||||
|
||||
## 🎯 Problem Identified & Fixed
|
||||
|
||||
**Root Cause:** The Vite proxy configuration was intercepting ALL `/auth/*` routes and forwarding them to the backend, including the OAuth callback route `/auth/google/callback` that needed to be handled by the React frontend.
|
||||
|
||||
## 🔧 Solution Applied
|
||||
|
||||
**Fixed Vite Configuration** (`frontend/vite.config.ts`):
|
||||
|
||||
**BEFORE (Problematic):**
|
||||
```typescript
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/auth': { // ❌ This was intercepting ALL /auth routes
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**AFTER (Fixed):**
|
||||
```typescript
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
// ✅ Only proxy specific auth endpoints, not the callback route
|
||||
'/auth/setup': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/auth/google/url': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/auth/google/exchange': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/auth/me': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/auth/logout': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/auth/status': {
|
||||
target: 'http://backend:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 How OAuth Flow Works Now
|
||||
|
||||
1. **User clicks "Continue with Google"**
|
||||
- Frontend calls `/auth/google/url` → Proxied to backend
|
||||
- Backend returns Google OAuth URL with correct redirect URI
|
||||
|
||||
2. **Google Authentication**
|
||||
- User authenticates with Google
|
||||
- Google redirects to: `https://bsa.madeamess.online:5173/auth/google/callback?code=...`
|
||||
|
||||
3. **Frontend Handles Callback** ✅
|
||||
- `/auth/google/callback` is NOT proxied to backend
|
||||
- React Router serves the frontend app
|
||||
- Login component detects callback route and authorization code
|
||||
- Calls `/auth/google/exchange` → Proxied to backend
|
||||
- Backend exchanges code for JWT token
|
||||
- Frontend receives token and user info, logs user in
|
||||
|
||||
## 🎉 Current Status
|
||||
|
||||
**✅ All containers running successfully**
|
||||
**✅ Vite proxy configuration fixed**
|
||||
**✅ OAuth callback route now handled by frontend**
|
||||
**✅ Backend OAuth endpoints working correctly**
|
||||
|
||||
## 🧪 Test the Fix
|
||||
|
||||
1. Visit your domain: `https://bsa.madeamess.online:5173`
|
||||
2. Click "Continue with Google"
|
||||
3. Complete Google authentication
|
||||
4. You should be redirected back and logged in successfully!
|
||||
|
||||
The OAuth callback handoff issue has been completely resolved! 🎊
|
||||
@@ -1,216 +0,0 @@
|
||||
# OAuth2 Setup for Frontend-Only Port (5173)
|
||||
|
||||
## 🎯 Configuration Overview
|
||||
|
||||
Since you're only forwarding port 5173, the OAuth flow has been configured to work entirely through the frontend:
|
||||
|
||||
**Current Setup:**
|
||||
- **Frontend**: `https://bsa.madeamess.online:5173` (publicly accessible)
|
||||
- **Backend**: `http://localhost:3000` (internal only)
|
||||
- **OAuth Redirect**: `https://bsa.madeamess.online:5173/auth/google/callback`
|
||||
|
||||
## 🔧 Google Cloud Console Configuration
|
||||
|
||||
**Update your OAuth2 client with this redirect URI:**
|
||||
```
|
||||
https://bsa.madeamess.online:5173/auth/google/callback
|
||||
```
|
||||
|
||||
**Authorized JavaScript Origins:**
|
||||
```
|
||||
https://bsa.madeamess.online:5173
|
||||
```
|
||||
|
||||
## 🔄 How the OAuth Flow Works
|
||||
|
||||
### 1. **Frontend Initiates OAuth**
|
||||
```javascript
|
||||
// Frontend calls backend to get OAuth URL
|
||||
const response = await fetch('/api/auth/google/url');
|
||||
const { url } = await response.json();
|
||||
window.location.href = url; // Redirect to Google
|
||||
```
|
||||
|
||||
### 2. **Google Redirects to Frontend**
|
||||
```
|
||||
https://bsa.madeamess.online:5173/auth/google/callback?code=AUTHORIZATION_CODE
|
||||
```
|
||||
|
||||
### 3. **Frontend Exchanges Code for Token**
|
||||
```javascript
|
||||
// Frontend sends code to backend
|
||||
const response = await fetch('/api/auth/google/exchange', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: authorizationCode })
|
||||
});
|
||||
|
||||
const { token, user } = await response.json();
|
||||
// Store token in localStorage or secure cookie
|
||||
```
|
||||
|
||||
## 🛠️ Backend API Endpoints
|
||||
|
||||
### **GET /api/auth/google/url**
|
||||
Returns the Google OAuth URL for frontend to redirect to.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=..."
|
||||
}
|
||||
```
|
||||
|
||||
### **POST /api/auth/google/exchange**
|
||||
Exchanges authorization code for JWT token.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"code": "authorization_code_from_google"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"token": "jwt_token_here",
|
||||
"user": {
|
||||
"id": "user_id",
|
||||
"email": "user@example.com",
|
||||
"name": "User Name",
|
||||
"picture": "profile_picture_url",
|
||||
"role": "coordinator"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **GET /api/auth/status**
|
||||
Check authentication status.
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Authorization: Bearer jwt_token_here
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 Frontend Implementation Example
|
||||
|
||||
### **Login Component**
|
||||
```javascript
|
||||
const handleGoogleLogin = async () => {
|
||||
try {
|
||||
// Get OAuth URL from backend
|
||||
const response = await fetch('/api/auth/google/url');
|
||||
const { url } = await response.json();
|
||||
|
||||
// Redirect to Google
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### **OAuth Callback Handler**
|
||||
```javascript
|
||||
// In your callback route component
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const error = urlParams.get('error');
|
||||
|
||||
if (error) {
|
||||
console.error('OAuth error:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
exchangeCodeForToken(code);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const exchangeCodeForToken = async (code) => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/google/exchange', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
|
||||
const { token, user } = await response.json();
|
||||
|
||||
// Store token securely
|
||||
localStorage.setItem('authToken', token);
|
||||
|
||||
// Redirect to dashboard
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Token exchange failed:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### **API Request Helper**
|
||||
```javascript
|
||||
const apiRequest = async (url, options = {}) => {
|
||||
const token = localStorage.getItem('authToken');
|
||||
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## 🚀 Testing the Setup
|
||||
|
||||
### 1. **Test OAuth URL Generation**
|
||||
```bash
|
||||
curl http://localhost:3000/api/auth/google/url
|
||||
```
|
||||
|
||||
### 2. **Test Full Flow**
|
||||
1. Visit: `https://bsa.madeamess.online:5173`
|
||||
2. Click login button
|
||||
3. Should redirect to Google
|
||||
4. After Google login, should redirect back to: `https://bsa.madeamess.online:5173/auth/google/callback?code=...`
|
||||
5. Frontend should exchange code for token
|
||||
6. User should be logged in
|
||||
|
||||
### 3. **Test API Access**
|
||||
```bash
|
||||
# Get a token first, then:
|
||||
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" http://localhost:3000/api/auth/status
|
||||
```
|
||||
|
||||
## ✅ Current Status
|
||||
|
||||
**✅ Containers Running:**
|
||||
- Backend: http://localhost:3000
|
||||
- Frontend: http://localhost:5173
|
||||
- Database: PostgreSQL on port 5432
|
||||
- Redis: Running on port 6379
|
||||
|
||||
**✅ OAuth Configuration:**
|
||||
- Redirect URI: `https://bsa.madeamess.online:5173/auth/google/callback`
|
||||
- Frontend URL: `https://bsa.madeamess.online:5173`
|
||||
- Backend endpoints ready for frontend integration
|
||||
|
||||
**🔄 Next Steps:**
|
||||
1. Update Google Cloud Console with the redirect URI
|
||||
2. Implement frontend OAuth handling
|
||||
3. Test the complete flow
|
||||
|
||||
The OAuth system is now properly configured to work through your frontend-only port setup! 🎉
|
||||
@@ -1,122 +0,0 @@
|
||||
# User Permission Issues - Debugging Summary
|
||||
|
||||
## Issues Found and Fixed
|
||||
|
||||
### 1. **Token Storage Inconsistency** ❌ → ✅
|
||||
**Problem**: Different components were using different localStorage keys for the authentication token:
|
||||
- `App.tsx` used `localStorage.getItem('authToken')`
|
||||
- `UserManagement.tsx` used `localStorage.getItem('token')` in one place
|
||||
|
||||
**Fix**: Standardized all components to use `'authToken'` as the localStorage key.
|
||||
|
||||
**Files Fixed**:
|
||||
- `frontend/src/components/UserManagement.tsx` - Line 69: Changed `localStorage.getItem('token')` to `localStorage.getItem('authToken')`
|
||||
|
||||
### 2. **Missing Authentication Headers in VIP Operations** ❌ → ✅
|
||||
**Problem**: The VIP management operations (add, edit, delete, fetch) were not including authentication headers, causing 401/403 errors.
|
||||
|
||||
**Fix**: Added proper authentication headers to all VIP API calls.
|
||||
|
||||
**Files Fixed**:
|
||||
- `frontend/src/pages/VipList.tsx`:
|
||||
- Added `apiCall` import from config
|
||||
- Updated `fetchVips()` to include `Authorization: Bearer ${token}` header
|
||||
- Updated `handleAddVip()` to include authentication headers
|
||||
- Updated `handleEditVip()` to include authentication headers
|
||||
- Updated `handleDeleteVip()` to include authentication headers
|
||||
- Fixed TypeScript error with EditVipForm props
|
||||
|
||||
### 3. **API URL Configuration** ✅
|
||||
**Status**: Already correctly configured
|
||||
- Frontend uses `https://api.bsa.madeamess.online` via `apiCall` helper
|
||||
- Backend has proper CORS configuration for the frontend domain
|
||||
|
||||
### 4. **Backend Authentication Middleware** ✅
|
||||
**Status**: Already properly implemented
|
||||
- VIP routes are protected with `requireAuth` middleware
|
||||
- Role-based access control with `requireRole(['coordinator', 'administrator'])`
|
||||
- User management routes require `administrator` role
|
||||
|
||||
## Backend Permission Structure (Already Working)
|
||||
|
||||
```typescript
|
||||
// VIP Operations - Require coordinator or administrator role
|
||||
app.post('/api/vips', requireAuth, requireRole(['coordinator', 'administrator']))
|
||||
app.put('/api/vips/:id', requireAuth, requireRole(['coordinator', 'administrator']))
|
||||
app.delete('/api/vips/:id', requireAuth, requireRole(['coordinator', 'administrator']))
|
||||
app.get('/api/vips', requireAuth) // All authenticated users can view
|
||||
|
||||
// User Management - Require administrator role only
|
||||
app.get('/auth/users', requireAuth, requireRole(['administrator']))
|
||||
app.patch('/auth/users/:email/role', requireAuth, requireRole(['administrator']))
|
||||
app.delete('/auth/users/:email', requireAuth, requireRole(['administrator']))
|
||||
```
|
||||
|
||||
## Role Hierarchy
|
||||
|
||||
1. **Administrator**:
|
||||
- Full access to all features
|
||||
- Can manage users and change roles
|
||||
- Can add/edit/delete VIPs
|
||||
- Can manage drivers and schedules
|
||||
|
||||
2. **Coordinator**:
|
||||
- Can add/edit/delete VIPs
|
||||
- Can manage drivers and schedules
|
||||
- Cannot manage users or change roles
|
||||
|
||||
3. **Driver**:
|
||||
- Can view assigned schedules
|
||||
- Can update status
|
||||
- Cannot manage VIPs or users
|
||||
|
||||
## Testing the Fixes
|
||||
|
||||
After these fixes, the admin should now be able to:
|
||||
|
||||
1. ✅ **Add VIPs**: The "Add New VIP" button will work with proper authentication
|
||||
2. ✅ **Change User Roles**: The role dropdown in User Management will work correctly
|
||||
3. ✅ **View All Data**: All API calls now include proper authentication headers
|
||||
|
||||
## What Was Happening Before
|
||||
|
||||
1. **VIP Operations Failing**: When clicking "Add New VIP" or trying to edit/delete VIPs, the requests were being sent without authentication headers, causing the backend to return 401 Unauthorized errors.
|
||||
|
||||
2. **User Role Changes Failing**: The user management component was using the wrong token storage key, so role update requests were failing with authentication errors.
|
||||
|
||||
3. **Silent Failures**: The frontend wasn't showing proper error messages, so it appeared that buttons weren't working when actually the API calls were being rejected.
|
||||
|
||||
## Additional Recommendations
|
||||
|
||||
1. **Error Handling**: Consider adding user-friendly error messages when API calls fail
|
||||
2. **Loading States**: Add loading indicators for user actions (role changes, VIP operations)
|
||||
3. **Token Refresh**: Implement token refresh logic for long-running sessions
|
||||
4. **Audit Logging**: Consider logging user actions for security auditing
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `frontend/src/components/UserManagement.tsx` - Fixed token storage key inconsistency
|
||||
2. `frontend/src/pages/VipList.tsx` - Added authentication headers to all VIP operations
|
||||
3. `frontend/src/pages/DriverList.tsx` - Added authentication headers to all driver operations
|
||||
4. `frontend/src/pages/Dashboard.tsx` - Added authentication headers to dashboard data fetching
|
||||
5. `vip-coordinator/PERMISSION_ISSUES_FIXED.md` - This documentation
|
||||
|
||||
## Site-Wide Authentication Fix
|
||||
|
||||
You were absolutely right - this was a site-wide problem! I've now fixed authentication headers across all major components:
|
||||
|
||||
### ✅ Fixed Components:
|
||||
- **VipList**: All CRUD operations (create, read, update, delete) now include auth headers
|
||||
- **DriverList**: All CRUD operations now include auth headers
|
||||
- **Dashboard**: Data fetching for VIPs, drivers, and schedules now includes auth headers
|
||||
- **UserManagement**: Token storage key fixed and all operations include auth headers
|
||||
|
||||
### 🔍 Components Still Needing Review:
|
||||
- `ScheduleManager.tsx` - Schedule operations
|
||||
- `DriverSelector.tsx` - Driver availability checks
|
||||
- `VipDetails.tsx` - VIP detail fetching
|
||||
- `DriverDashboard.tsx` - Driver schedule operations
|
||||
- `FlightStatus.tsx` - Flight data fetching
|
||||
- `VipForm.tsx` & `EditVipForm.tsx` - Flight validation
|
||||
|
||||
The permission system is now working correctly with proper authentication and authorization for the main management operations!
|
||||
@@ -1,173 +0,0 @@
|
||||
# 🚀 Port 3000 Direct Access Setup Guide
|
||||
|
||||
## Your Optimal Setup (Based on Google's AI Analysis)
|
||||
|
||||
Google's AI correctly identified that the OAuth redirect to `localhost:3000` is the issue. Here's the **simplest solution**:
|
||||
|
||||
## Option A: Expose Port 3000 Directly (Recommended)
|
||||
|
||||
### 1. Router/Firewall Configuration
|
||||
Configure your router to forward **both ports**:
|
||||
|
||||
```
|
||||
Internet → Router → Your Server
|
||||
Port 443/80 → Frontend (port 5173) ✅ Already working
|
||||
Port 3000 → Backend (port 3000) ⚠️ ADD THIS
|
||||
```
|
||||
|
||||
### 2. Google Cloud Console Update
|
||||
|
||||
**Authorized JavaScript origins:**
|
||||
```
|
||||
https://bsa.madeamess.online
|
||||
https://bsa.madeamess.online:3000
|
||||
```
|
||||
|
||||
**Authorized redirect URIs:**
|
||||
```
|
||||
https://bsa.madeamess.online:3000/auth/google/callback
|
||||
```
|
||||
|
||||
### 3. Environment Variables (Already Updated)
|
||||
✅ I've already updated your `.env` file:
|
||||
```bash
|
||||
GOOGLE_REDIRECT_URI=https://bsa.madeamess.online:3000/auth/google/callback
|
||||
FRONTEND_URL=https://bsa.madeamess.online
|
||||
```
|
||||
|
||||
### 4. SSL Certificate for Port 3000
|
||||
You'll need SSL on port 3000. Options:
|
||||
|
||||
**Option A: Reverse proxy for port 3000 too**
|
||||
```nginx
|
||||
# Frontend (existing)
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name bsa.madeamess.online;
|
||||
location / {
|
||||
proxy_pass http://localhost:5173;
|
||||
}
|
||||
}
|
||||
|
||||
# Backend (add this)
|
||||
server {
|
||||
listen 3000 ssl;
|
||||
server_name bsa.madeamess.online;
|
||||
ssl_certificate /path/to/your/cert.pem;
|
||||
ssl_certificate_key /path/to/your/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Option B: Direct Docker port mapping with SSL termination**
|
||||
```yaml
|
||||
# In docker-compose.dev.yml
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- SSL_CERT_PATH=/certs/cert.pem
|
||||
- SSL_KEY_PATH=/certs/key.pem
|
||||
```
|
||||
|
||||
## Option B: Alternative - Use Standard HTTPS Port
|
||||
|
||||
If you don't want to expose port 3000, use a subdomain:
|
||||
|
||||
### 1. Create Subdomain
|
||||
Point `api.bsa.madeamess.online` to your server
|
||||
|
||||
### 2. Update Environment Variables
|
||||
```bash
|
||||
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
|
||||
```
|
||||
|
||||
### 3. Configure Reverse Proxy
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name api.bsa.madeamess.online;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
# ... headers
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Your Setup
|
||||
|
||||
### 1. Restart Containers
|
||||
```bash
|
||||
cd /home/kyle/Desktop/vip-coordinator
|
||||
docker-compose -f docker-compose.dev.yml restart
|
||||
```
|
||||
|
||||
### 2. Test Backend Accessibility
|
||||
```bash
|
||||
# Should work from internet
|
||||
curl https://bsa.madeamess.online:3000/auth/setup
|
||||
# Should return: {"setupCompleted":true,"firstAdminCreated":false,"oauthConfigured":true}
|
||||
```
|
||||
|
||||
### 3. Test OAuth URL Generation
|
||||
```bash
|
||||
curl https://bsa.madeamess.online:3000/auth/google/url
|
||||
# Should return Google OAuth URL with correct redirect_uri
|
||||
```
|
||||
|
||||
### 4. Test Complete OAuth Flow
|
||||
1. Visit `https://bsa.madeamess.online` (frontend)
|
||||
2. Click "Continue with Google"
|
||||
3. Google redirects to `https://bsa.madeamess.online:3000/auth/google/callback`
|
||||
4. Backend processes OAuth and redirects back to frontend with token
|
||||
5. User is authenticated ✅
|
||||
|
||||
## Why This Works Better
|
||||
|
||||
✅ **Direct backend access** - Google can reach your OAuth callback
|
||||
✅ **Simpler configuration** - No complex reverse proxy routing
|
||||
✅ **Easier debugging** - Clear separation of frontend/backend
|
||||
✅ **Standard OAuth flow** - Follows OAuth 2.0 best practices
|
||||
|
||||
## Security Considerations
|
||||
|
||||
🔒 **SSL Required**: Port 3000 must use HTTPS for OAuth
|
||||
🔒 **Firewall Rules**: Only expose necessary ports
|
||||
🔒 **CORS Configuration**: Already configured for your domain
|
||||
|
||||
## Quick Commands
|
||||
|
||||
```bash
|
||||
# 1. Restart containers with new config
|
||||
docker-compose -f docker-compose.dev.yml restart
|
||||
|
||||
# 2. Test backend
|
||||
curl https://bsa.madeamess.online:3000/auth/setup
|
||||
|
||||
# 3. Check OAuth URL
|
||||
curl https://bsa.madeamess.online:3000/auth/google/url
|
||||
|
||||
# 4. Test frontend
|
||||
curl https://bsa.madeamess.online
|
||||
```
|
||||
|
||||
## Expected Flow After Setup
|
||||
|
||||
1. **User visits**: `https://bsa.madeamess.online` (frontend)
|
||||
2. **Clicks login**: Frontend calls `https://bsa.madeamess.online:3000/auth/google/url`
|
||||
3. **Redirects to Google**: User authenticates with Google
|
||||
4. **Google redirects back**: `https://bsa.madeamess.online:3000/auth/google/callback`
|
||||
5. **Backend processes**: Creates JWT token
|
||||
6. **Redirects to frontend**: `https://bsa.madeamess.online/auth/callback?token=...`
|
||||
7. **Frontend receives token**: User is logged in ✅
|
||||
|
||||
This setup will resolve the OAuth callback issue you're experiencing!
|
||||
@@ -1,199 +0,0 @@
|
||||
# 🐘 PostgreSQL User Management System
|
||||
|
||||
## ✅ What We Built
|
||||
|
||||
A **production-ready user management system** using your existing PostgreSQL database infrastructure with proper database design, indexing, and transactional operations.
|
||||
|
||||
## 🎯 Database Architecture
|
||||
|
||||
### **Users Table Schema**
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
picture TEXT,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'coordinator',
|
||||
provider VARCHAR(50) NOT NULL DEFAULT 'google',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_sign_in_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- Optimized indexes for performance
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
```
|
||||
|
||||
### **Key Features**
|
||||
- ✅ **Primary key constraints** - Unique user identification
|
||||
- ✅ **Email uniqueness** - Prevents duplicate accounts
|
||||
- ✅ **Proper indexing** - Fast lookups by email and role
|
||||
- ✅ **Timezone-aware timestamps** - Accurate time tracking
|
||||
- ✅ **Default values** - Sensible defaults for new users
|
||||
|
||||
## 🚀 System Components
|
||||
|
||||
### **1. DatabaseService (`databaseService.ts`)**
|
||||
- **Connection pooling** with PostgreSQL
|
||||
- **Automatic schema initialization** on startup
|
||||
- **Transactional operations** for data consistency
|
||||
- **Error handling** and connection management
|
||||
- **Future-ready** with VIP and schedule tables
|
||||
|
||||
### **2. Enhanced Auth Routes (`simpleAuth.ts`)**
|
||||
- **Async/await** for all database operations
|
||||
- **Proper error handling** with database fallbacks
|
||||
- **User creation** with automatic role assignment
|
||||
- **Login tracking** with timestamp updates
|
||||
- **Role-based access control** for admin operations
|
||||
|
||||
### **3. User Management API**
|
||||
```typescript
|
||||
// List all users (admin only)
|
||||
GET /auth/users
|
||||
|
||||
// Update user role (admin only)
|
||||
PATCH /auth/users/:email/role
|
||||
Body: { "role": "administrator" | "coordinator" | "driver" }
|
||||
|
||||
// Delete user (admin only)
|
||||
DELETE /auth/users/:email
|
||||
|
||||
// Get specific user (admin only)
|
||||
GET /auth/users/:email
|
||||
```
|
||||
|
||||
### **4. Frontend Interface (`UserManagement.tsx`)**
|
||||
- **Real-time data** from PostgreSQL
|
||||
- **Professional UI** with loading states
|
||||
- **Error handling** with user feedback
|
||||
- **Role management** with instant updates
|
||||
- **Responsive design** for all screen sizes
|
||||
|
||||
## 🔧 Technical Advantages
|
||||
|
||||
### **Database Benefits:**
|
||||
- ✅ **ACID compliance** - Guaranteed data consistency
|
||||
- ✅ **Concurrent access** - Multiple users safely
|
||||
- ✅ **Backup & recovery** - Enterprise-grade data protection
|
||||
- ✅ **Scalability** - Handles thousands of users
|
||||
- ✅ **Query optimization** - Indexed for performance
|
||||
|
||||
### **Security Features:**
|
||||
- ✅ **SQL injection protection** - Parameterized queries
|
||||
- ✅ **Connection pooling** - Efficient resource usage
|
||||
- ✅ **Role validation** - Server-side permission checks
|
||||
- ✅ **Transaction safety** - Atomic operations
|
||||
|
||||
### **Production Ready:**
|
||||
- ✅ **Error handling** - Graceful failure recovery
|
||||
- ✅ **Logging** - Comprehensive operation tracking
|
||||
- ✅ **Connection management** - Automatic reconnection
|
||||
- ✅ **Schema migration** - Safe database updates
|
||||
|
||||
## 📋 Setup & Usage
|
||||
|
||||
### **1. Database Initialization**
|
||||
The system automatically creates tables on startup:
|
||||
```bash
|
||||
# Your existing Docker setup handles this
|
||||
docker-compose -f docker-compose.dev.yml up
|
||||
```
|
||||
|
||||
### **2. First User Setup**
|
||||
- **First user** becomes administrator automatically
|
||||
- **Subsequent users** become coordinators by default
|
||||
- **Role changes** can be made through admin interface
|
||||
|
||||
### **3. User Management Workflow**
|
||||
1. **Login with Google OAuth** - Users authenticate via Google
|
||||
2. **Automatic user creation** - New users added to database
|
||||
3. **Role assignment** - Admin can change user roles
|
||||
4. **Permission enforcement** - Role-based access control
|
||||
5. **User lifecycle** - Full CRUD operations for admins
|
||||
|
||||
## 🎯 Database Operations
|
||||
|
||||
### **User Creation Flow:**
|
||||
```sql
|
||||
-- Check if user exists
|
||||
SELECT * FROM users WHERE email = $1;
|
||||
|
||||
-- Create new user if not exists
|
||||
INSERT INTO users (id, email, name, picture, role, provider, last_sign_in_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
|
||||
RETURNING *;
|
||||
```
|
||||
|
||||
### **Role Update Flow:**
|
||||
```sql
|
||||
-- Update user role with timestamp
|
||||
UPDATE users
|
||||
SET role = $1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE email = $2
|
||||
RETURNING *;
|
||||
```
|
||||
|
||||
### **Login Tracking:**
|
||||
```sql
|
||||
-- Update last sign-in timestamp
|
||||
UPDATE users
|
||||
SET last_sign_in_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE email = $1
|
||||
RETURNING *;
|
||||
```
|
||||
|
||||
## 🔍 Monitoring & Maintenance
|
||||
|
||||
### **Database Health:**
|
||||
- **Connection status** logged on startup
|
||||
- **Query performance** tracked in logs
|
||||
- **Error handling** with detailed logging
|
||||
- **Connection pooling** metrics available
|
||||
|
||||
### **User Analytics:**
|
||||
- **User count** tracking for admin setup
|
||||
- **Login patterns** via last_sign_in_at
|
||||
- **Role distribution** via role indexing
|
||||
- **Account creation** trends via created_at
|
||||
|
||||
## 🚀 Future Enhancements
|
||||
|
||||
### **Ready for Extension:**
|
||||
- **User profiles** - Additional metadata fields
|
||||
- **User groups** - Team-based permissions
|
||||
- **Audit logging** - Track all user actions
|
||||
- **Session management** - Advanced security
|
||||
- **Multi-factor auth** - Enhanced security
|
||||
|
||||
### **Database Scaling:**
|
||||
- **Read replicas** - For high-traffic scenarios
|
||||
- **Partitioning** - For large user bases
|
||||
- **Caching** - Redis integration ready
|
||||
- **Backup strategies** - Automated backups
|
||||
|
||||
## 🎉 Production Benefits
|
||||
|
||||
### **Enterprise Grade:**
|
||||
- ✅ **Reliable** - PostgreSQL battle-tested reliability
|
||||
- ✅ **Scalable** - Handles growth from 10 to 10,000+ users
|
||||
- ✅ **Secure** - Industry-standard security practices
|
||||
- ✅ **Maintainable** - Clean, documented codebase
|
||||
|
||||
### **Developer Friendly:**
|
||||
- ✅ **Type-safe** - Full TypeScript integration
|
||||
- ✅ **Well-documented** - Clear API and database schema
|
||||
- ✅ **Error-handled** - Graceful failure modes
|
||||
- ✅ **Testable** - Isolated database operations
|
||||
|
||||
Your user management system is now **production-ready** with enterprise-grade PostgreSQL backing! 🚀
|
||||
|
||||
## 🔧 Quick Start
|
||||
|
||||
1. **Ensure PostgreSQL is running** (your Docker setup handles this)
|
||||
2. **Restart your backend** to initialize tables
|
||||
3. **Login as first user** to become administrator
|
||||
4. **Manage users** through the beautiful admin interface
|
||||
|
||||
All user data is now safely stored in PostgreSQL with proper indexing, relationships, and ACID compliance!
|
||||
239
QUICKSTART.md
Normal file
239
QUICKSTART.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# VIP Coordinator - Quick Start Guide
|
||||
|
||||
## 🚀 Get Started in 5 Minutes
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- Docker Desktop
|
||||
- Auth0 Account (free tier at https://auth0.com)
|
||||
|
||||
### Step 1: Start Database
|
||||
|
||||
```bash
|
||||
cd vip-coordinator
|
||||
docker-compose up -d postgres
|
||||
```
|
||||
|
||||
### Step 2: Configure Auth0
|
||||
|
||||
1. Go to https://auth0.com and create a free account
|
||||
2. Create a new **Application** (Single Page Application)
|
||||
3. Create a new **API**
|
||||
4. Note your credentials:
|
||||
- Domain: `your-tenant.us.auth0.com`
|
||||
- Client ID: `abc123...`
|
||||
- Audience: `https://your-api-identifier`
|
||||
|
||||
5. Configure callback URLs in Auth0 dashboard:
|
||||
- **Allowed Callback URLs:** `http://localhost:5173/callback`
|
||||
- **Allowed Logout URLs:** `http://localhost:5173`
|
||||
- **Allowed Web Origins:** `http://localhost:5173`
|
||||
|
||||
### Step 3: Configure Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# Edit .env file
|
||||
# Replace these with your Auth0 credentials:
|
||||
AUTH0_DOMAIN="your-tenant.us.auth0.com"
|
||||
AUTH0_AUDIENCE="https://your-api-identifier"
|
||||
AUTH0_ISSUER="https://your-tenant.us.auth0.com/"
|
||||
|
||||
# Install and setup
|
||||
npm install
|
||||
npx prisma generate
|
||||
npx prisma migrate dev
|
||||
npm run prisma:seed
|
||||
```
|
||||
|
||||
### Step 4: Configure Frontend
|
||||
|
||||
```bash
|
||||
cd ../frontend
|
||||
|
||||
# Edit .env file
|
||||
# Replace these with your Auth0 credentials:
|
||||
VITE_AUTH0_DOMAIN="your-tenant.us.auth0.com"
|
||||
VITE_AUTH0_CLIENT_ID="your-client-id"
|
||||
VITE_AUTH0_AUDIENCE="https://your-api-identifier"
|
||||
|
||||
# Already installed during build
|
||||
# npm install (only if not already done)
|
||||
```
|
||||
|
||||
### Step 5: Start Everything
|
||||
|
||||
```bash
|
||||
# Terminal 1: Backend
|
||||
cd backend
|
||||
npm run start:dev
|
||||
|
||||
# Terminal 2: Frontend
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Step 6: Access the App
|
||||
|
||||
Open your browser to: **http://localhost:5173**
|
||||
|
||||
1. Click "Sign In with Auth0"
|
||||
2. Create an account or sign in
|
||||
3. **First user becomes Administrator automatically!**
|
||||
4. Explore the dashboard
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What You Get
|
||||
|
||||
### Backend API (http://localhost:3000/api/v1)
|
||||
|
||||
- ✅ **Auth0 Authentication** - Secure JWT-based auth
|
||||
- ✅ **User Management** - Approval workflow for new users
|
||||
- ✅ **VIP Management** - Complete CRUD with relationships
|
||||
- ✅ **Driver Management** - Driver profiles and schedules
|
||||
- ✅ **Event Scheduling** - Smart conflict detection
|
||||
- ✅ **Flight Tracking** - Real-time flight status (AviationStack API)
|
||||
- ✅ **40+ API Endpoints** - Fully documented REST API
|
||||
- ✅ **Role-Based Access** - Administrator, Coordinator, Driver
|
||||
- ✅ **Sample Data** - Pre-loaded test data
|
||||
|
||||
### Frontend (http://localhost:5173)
|
||||
|
||||
- ✅ **Modern React UI** - React 18 + TypeScript
|
||||
- ✅ **Tailwind CSS** - Beautiful, responsive design
|
||||
- ✅ **Auth0 Integration** - Seamless authentication
|
||||
- ✅ **TanStack Query** - Smart data fetching and caching
|
||||
- ✅ **Dashboard** - Overview with stats and recent activity
|
||||
- ✅ **VIP Management** - List, view, create, edit VIPs
|
||||
- ✅ **Driver Management** - Manage driver profiles
|
||||
- ✅ **Schedule View** - See all events and assignments
|
||||
- ✅ **Protected Routes** - Automatic authentication checks
|
||||
|
||||
---
|
||||
|
||||
## 📊 Sample Data
|
||||
|
||||
The database is seeded with:
|
||||
|
||||
- **2 Users:** admin@example.com, coordinator@example.com
|
||||
- **2 VIPs:** Dr. Robert Johnson (flight), Ms. Sarah Williams (self-driving)
|
||||
- **2 Drivers:** John Smith, Jane Doe
|
||||
- **3 Events:** Airport pickup, welcome dinner, conference transport
|
||||
|
||||
---
|
||||
|
||||
## 🔑 User Roles
|
||||
|
||||
### Administrator
|
||||
- Full system access
|
||||
- Can approve/deny new users
|
||||
- Can manage all VIPs, drivers, events
|
||||
|
||||
### Coordinator
|
||||
- Can manage VIPs, drivers, events
|
||||
- Cannot manage users
|
||||
- Full scheduling access
|
||||
|
||||
### Driver
|
||||
- View assigned schedules
|
||||
- Update event status
|
||||
- Cannot create or delete
|
||||
|
||||
**First user to register = Administrator** (no manual setup needed!)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing the API
|
||||
|
||||
### Health Check (Public)
|
||||
```bash
|
||||
curl http://localhost:3000/api/v1/health
|
||||
```
|
||||
|
||||
### Get Profile (Requires Auth0 Token)
|
||||
```bash
|
||||
# Get token from browser DevTools -> Application -> Local Storage -> auth0_token
|
||||
curl http://localhost:3000/api/v1/auth/profile \
|
||||
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
||||
```
|
||||
|
||||
### List VIPs
|
||||
```bash
|
||||
curl http://localhost:3000/api/v1/vips \
|
||||
-H "Authorization: Bearer YOUR_TOKEN_HERE"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### "Cannot connect to database"
|
||||
```bash
|
||||
# Check PostgreSQL is running
|
||||
docker ps | grep postgres
|
||||
|
||||
# Should see: vip-postgres running on port 5433
|
||||
```
|
||||
|
||||
### "Auth0 redirect loop"
|
||||
- Check your `.env` files have correct Auth0 credentials
|
||||
- Verify callback URLs in Auth0 dashboard match `http://localhost:5173/callback`
|
||||
- Clear browser cache and cookies
|
||||
|
||||
### "Cannot find module"
|
||||
```bash
|
||||
# Backend
|
||||
cd backend
|
||||
npx prisma generate
|
||||
npm run build
|
||||
|
||||
# Frontend
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
### "Port already in use"
|
||||
- Backend uses port 3000
|
||||
- Frontend uses port 5173
|
||||
- PostgreSQL uses port 5433
|
||||
|
||||
Close any processes using these ports.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
1. **Explore the Dashboard** - See stats and recent activity
|
||||
2. **Add a VIP** - Try creating a new VIP profile
|
||||
3. **Assign a Driver** - Schedule an event with driver assignment
|
||||
4. **Test Conflict Detection** - Try double-booking a driver
|
||||
5. **Approve Users** - Have someone else sign up, then approve them as admin
|
||||
6. **View API Docs** - Check [backend/README.md](backend/README.md)
|
||||
|
||||
---
|
||||
|
||||
## 🚢 Deploy to Production
|
||||
|
||||
See [CLAUDE.md](CLAUDE.md) for Digital Ocean deployment instructions.
|
||||
|
||||
Ready to deploy:
|
||||
- ✅ Docker Compose configuration
|
||||
- ✅ Production environment variables
|
||||
- ✅ Optimized builds
|
||||
- ✅ Auth0 production setup guide
|
||||
|
||||
---
|
||||
|
||||
**Need Help?**
|
||||
|
||||
- Check [CLAUDE.md](CLAUDE.md) for comprehensive documentation
|
||||
- Check [README.md](README.md) for detailed feature overview
|
||||
- Check [backend/README.md](backend/README.md) for API docs
|
||||
- Check [frontend/README.md](frontend/README.md) for frontend docs
|
||||
|
||||
**Built with:** NestJS, React, TypeScript, Prisma, PostgreSQL, Auth0, Tailwind CSS
|
||||
|
||||
**Last Updated:** January 25, 2026
|
||||
218
README-API.md
218
README-API.md
@@ -1,218 +0,0 @@
|
||||
# VIP Coordinator API Documentation
|
||||
|
||||
## 📚 Overview
|
||||
|
||||
This document provides comprehensive API documentation for the VIP Coordinator system using **OpenAPI 3.0** (Swagger) specification. The API enables management of VIP transportation coordination, including flight tracking, driver management, and event scheduling.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### View API Documentation
|
||||
|
||||
1. **Interactive Documentation (Recommended):**
|
||||
```bash
|
||||
# Open the interactive Swagger UI documentation
|
||||
open vip-coordinator/api-docs.html
|
||||
```
|
||||
Or visit: `file:///path/to/vip-coordinator/api-docs.html`
|
||||
|
||||
2. **Raw OpenAPI Specification:**
|
||||
```bash
|
||||
# View the YAML specification file
|
||||
cat vip-coordinator/api-documentation.yaml
|
||||
```
|
||||
|
||||
### Test the API
|
||||
|
||||
The interactive documentation includes a "Try it out" feature that allows you to test endpoints directly:
|
||||
|
||||
1. Open `api-docs.html` in your browser
|
||||
2. Click on any endpoint to expand it
|
||||
3. Click "Try it out" button
|
||||
4. Fill in parameters and request body
|
||||
5. Click "Execute" to make the API call
|
||||
|
||||
## 📋 API Categories
|
||||
|
||||
### 🏥 Health
|
||||
- `GET /api/health` - System health check
|
||||
|
||||
### 👥 VIPs
|
||||
- `GET /api/vips` - Get all VIPs
|
||||
- `POST /api/vips` - Create new VIP
|
||||
- `PUT /api/vips/{id}` - Update VIP
|
||||
- `DELETE /api/vips/{id}` - Delete VIP
|
||||
|
||||
### 🚗 Drivers
|
||||
- `GET /api/drivers` - Get all drivers
|
||||
- `POST /api/drivers` - Create new driver
|
||||
- `PUT /api/drivers/{id}` - Update driver
|
||||
- `DELETE /api/drivers/{id}` - Delete driver
|
||||
- `GET /api/drivers/{driverId}/schedule` - Get driver's schedule
|
||||
- `POST /api/drivers/availability` - Check driver availability
|
||||
- `POST /api/drivers/{driverId}/conflicts` - Check driver conflicts
|
||||
|
||||
### ✈️ Flights
|
||||
- `GET /api/flights/{flightNumber}` - Get flight information
|
||||
- `POST /api/flights/{flightNumber}/track` - Start flight tracking
|
||||
- `DELETE /api/flights/{flightNumber}/track` - Stop flight tracking
|
||||
- `POST /api/flights/batch` - Get multiple flights info
|
||||
- `GET /api/flights/tracking/status` - Get tracking status
|
||||
|
||||
### 📅 Schedule
|
||||
- `GET /api/vips/{vipId}/schedule` - Get VIP's schedule
|
||||
- `POST /api/vips/{vipId}/schedule` - Add event to schedule
|
||||
- `PUT /api/vips/{vipId}/schedule/{eventId}` - Update event
|
||||
- `DELETE /api/vips/{vipId}/schedule/{eventId}` - Delete event
|
||||
- `PATCH /api/vips/{vipId}/schedule/{eventId}/status` - Update event status
|
||||
|
||||
### ⚙️ Admin
|
||||
- `POST /api/admin/authenticate` - Admin authentication
|
||||
- `GET /api/admin/settings` - Get admin settings
|
||||
- `POST /api/admin/settings` - Update admin settings
|
||||
|
||||
## 💡 Example API Calls
|
||||
|
||||
### Create a VIP with Flight
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/vips \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "John Doe",
|
||||
"organization": "Tech Corp",
|
||||
"transportMode": "flight",
|
||||
"flights": [
|
||||
{
|
||||
"flightNumber": "UA1234",
|
||||
"flightDate": "2025-06-26",
|
||||
"segment": 1
|
||||
}
|
||||
],
|
||||
"needsAirportPickup": true,
|
||||
"needsVenueTransport": true,
|
||||
"notes": "CEO - requires executive transport"
|
||||
}'
|
||||
```
|
||||
|
||||
### Add Event to VIP Schedule
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/vips/{vipId}/schedule \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"title": "Meeting with CEO",
|
||||
"location": "Hyatt Regency Denver",
|
||||
"startTime": "2025-06-26T11:00:00",
|
||||
"endTime": "2025-06-26T12:30:00",
|
||||
"type": "meeting",
|
||||
"assignedDriverId": "1748780965562",
|
||||
"description": "Important strategic meeting"
|
||||
}'
|
||||
```
|
||||
|
||||
### Check Driver Availability
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/drivers/availability \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"startTime": "2025-06-26T11:00:00",
|
||||
"endTime": "2025-06-26T12:30:00",
|
||||
"location": "Denver Convention Center"
|
||||
}'
|
||||
```
|
||||
|
||||
### Get Flight Information
|
||||
```bash
|
||||
curl "http://localhost:3000/api/flights/UA1234?date=2025-06-26"
|
||||
```
|
||||
|
||||
## 🔧 Tools for API Documentation
|
||||
|
||||
### 1. **Swagger UI (Recommended)**
|
||||
- **What it is:** Interactive web-based API documentation
|
||||
- **Features:**
|
||||
- Try endpoints directly in browser
|
||||
- Auto-generated from OpenAPI spec
|
||||
- Beautiful, responsive interface
|
||||
- Request/response examples
|
||||
- **Access:** Open `api-docs.html` in your browser
|
||||
|
||||
### 2. **OpenAPI Specification**
|
||||
- **What it is:** Industry-standard API specification format
|
||||
- **Features:**
|
||||
- Machine-readable API definition
|
||||
- Can generate client SDKs
|
||||
- Supports validation and testing
|
||||
- Compatible with many tools
|
||||
- **File:** `api-documentation.yaml`
|
||||
|
||||
### 3. **Alternative Tools**
|
||||
|
||||
You can use the OpenAPI specification with other tools:
|
||||
|
||||
#### Postman
|
||||
1. Import `api-documentation.yaml` into Postman
|
||||
2. Automatically creates a collection with all endpoints
|
||||
3. Includes examples and validation
|
||||
|
||||
#### Insomnia
|
||||
1. Import the OpenAPI spec
|
||||
2. Generate requests automatically
|
||||
3. Built-in environment management
|
||||
|
||||
#### VS Code Extensions
|
||||
- **OpenAPI (Swagger) Editor** - Edit and preview API specs
|
||||
- **REST Client** - Test APIs directly in VS Code
|
||||
|
||||
## 📖 Documentation Best Practices
|
||||
|
||||
### Why OpenAPI/Swagger?
|
||||
|
||||
1. **Industry Standard:** Most widely adopted API documentation format
|
||||
2. **Interactive:** Users can test APIs directly in the documentation
|
||||
3. **Code Generation:** Can generate client libraries in multiple languages
|
||||
4. **Validation:** Ensures API requests/responses match specification
|
||||
5. **Tooling:** Extensive ecosystem of tools and integrations
|
||||
|
||||
### Documentation Features
|
||||
|
||||
- **Comprehensive:** All endpoints, parameters, and responses documented
|
||||
- **Examples:** Real-world examples for all operations
|
||||
- **Schemas:** Detailed data models with validation rules
|
||||
- **Error Handling:** Clear error response documentation
|
||||
- **Authentication:** Security requirements clearly specified
|
||||
|
||||
## 🔗 Integration Examples
|
||||
|
||||
### Frontend Integration
|
||||
```javascript
|
||||
// Example: Fetch VIPs in React
|
||||
const fetchVips = async () => {
|
||||
const response = await fetch('/api/vips');
|
||||
const vips = await response.json();
|
||||
return vips;
|
||||
};
|
||||
```
|
||||
|
||||
### Backend Integration
|
||||
```bash
|
||||
# Example: Using curl to test endpoints
|
||||
curl -X GET http://localhost:3000/api/health
|
||||
curl -X GET http://localhost:3000/api/vips
|
||||
curl -X GET http://localhost:3000/api/drivers
|
||||
```
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
1. **Explore the Interactive Docs:** Open `api-docs.html` and try the endpoints
|
||||
2. **Test with Real Data:** Use the populated test data to explore functionality
|
||||
3. **Build Integrations:** Use the API specification to build client applications
|
||||
4. **Extend the API:** Add new endpoints following the established patterns
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For questions about the API:
|
||||
- Review the interactive documentation
|
||||
- Check the OpenAPI specification for detailed schemas
|
||||
- Test endpoints using the "Try it out" feature
|
||||
- Refer to the example requests and responses
|
||||
|
||||
The API documentation is designed to be self-service and comprehensive, providing everything needed to integrate with the VIP Coordinator system.
|
||||
853
README.md
853
README.md
@@ -1,234 +1,735 @@
|
||||
# VIP Coordinator
|
||||
|
||||
A comprehensive web application for managing VIP logistics, driver assignments, and real-time tracking with Google OAuth authentication and role-based access control.
|
||||
> **Enterprise VIP & Transportation Management System for BSA Jamborees**
|
||||
|
||||
## ✨ Features
|
||||
A comprehensive full-stack application for coordinating VIP transportation, scheduling, and logistics at large-scale scouting events. Built with NestJS, React, PostgreSQL, and designed specifically for BSA (Boy Scouts of America) Jamboree operations.
|
||||
|
||||
### 🔐 Authentication & User Management
|
||||
- **Google OAuth Integration**: Secure login with Google accounts
|
||||
- **Role-Based Access Control**: Administrator, Coordinator, and Driver roles
|
||||
- **User Approval System**: Admin approval required for new users
|
||||
- **JWT-Based Authentication**: Stateless, secure token system
|
||||
---
|
||||
|
||||
### 👥 VIP Management
|
||||
- **Complete VIP Profiles**: Name, organization, department, transport details
|
||||
- **Multi-Flight Support**: Handle complex itineraries with multiple flights
|
||||
- **Department Organization**: Office of Development and Admin departments
|
||||
- **Schedule Management**: Event scheduling with conflict detection
|
||||
- **Real-time Flight Tracking**: Automatic flight status updates
|
||||
## 🎯 Overview
|
||||
|
||||
### 🚗 Driver Coordination
|
||||
- **Driver Management**: Create and manage driver profiles
|
||||
- **Availability Checking**: Real-time conflict detection
|
||||
- **Schedule Assignment**: Assign drivers to VIP events
|
||||
- **Department-Based Organization**: Organize drivers by department
|
||||
VIP Coordinator streamlines the complex logistics of managing hundreds of VIPs during multi-day events. It handles:
|
||||
|
||||
### ✈️ Flight Integration
|
||||
- **Real-time Flight Data**: Integration with AviationStack API
|
||||
- **Automatic Tracking**: Scheduled flight status updates
|
||||
- **Multi-Flight Support**: Handle complex travel itineraries
|
||||
- **Flight Validation**: Verify flight numbers and dates
|
||||
- **Multi-VIP Activity Scheduling** - Coordinate transport and activities for multiple VIPs simultaneously
|
||||
- **Real-time Driver Assignment** - Assign and reassign drivers on-the-fly with conflict detection
|
||||
- **Intelligent Search & Filtering** - Find activities instantly by VIP name, location, or type
|
||||
- **Resource Optimization** - Track vehicle capacity utilization (e.g., "3/6 seats used")
|
||||
- **Complete Itineraries** - Generate detailed 3-day schedules per VIP with all activities
|
||||
- **Role-Based Access Control** - Administrator, Coordinator, and Driver roles with granular permissions
|
||||
|
||||
### 📊 Advanced Features
|
||||
- **Interactive API Documentation**: Swagger UI with "Try it out" functionality
|
||||
- **Schedule Validation**: Prevent conflicts and overlapping assignments
|
||||
- **Comprehensive Logging**: Detailed system activity tracking
|
||||
- **Docker Containerization**: Easy deployment and development
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Backend
|
||||
- **Node.js + Express.js**: RESTful API server
|
||||
- **TypeScript**: Full type safety
|
||||
- **PostgreSQL**: Persistent data storage with automatic schema management
|
||||
- **Redis**: Caching and real-time updates
|
||||
- **JWT Authentication**: Secure, stateless authentication
|
||||
- **Google OAuth 2.0**: Simple, secure user authentication
|
||||
|
||||
### Frontend
|
||||
- **React 18 + TypeScript**: Modern, type-safe frontend
|
||||
- **Vite**: Lightning-fast development server
|
||||
- **Tailwind CSS v4**: Modern utility-first styling
|
||||
- **React Router**: Client-side routing
|
||||
- **Responsive Design**: Mobile-friendly interface
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Docker and Docker Compose
|
||||
- Google Cloud Console account (for OAuth setup)
|
||||
|
||||
### 1. Start the Application
|
||||
- **Node.js** 18+ and npm
|
||||
- **PostgreSQL** 16+
|
||||
- **Redis** 7+
|
||||
- **Auth0 Account** (for authentication)
|
||||
- **Docker** (optional, for containerized deployment)
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
# Clone the repository
|
||||
git clone http://192.168.68.53:3000/kyle/vip-coordinator.git
|
||||
cd vip-coordinator
|
||||
make dev
|
||||
|
||||
# === Backend Setup ===
|
||||
cd backend
|
||||
npm install
|
||||
|
||||
# Configure environment variables
|
||||
cp .env.example .env
|
||||
# Edit .env with your Auth0 credentials and database URL
|
||||
|
||||
# Run database migrations
|
||||
npx prisma migrate deploy
|
||||
|
||||
# Seed database with test data
|
||||
npx prisma db seed
|
||||
|
||||
# Start backend server (port 3000)
|
||||
npm run start:dev
|
||||
|
||||
# === Frontend Setup ===
|
||||
cd ../frontend
|
||||
npm install
|
||||
|
||||
# Configure environment variables
|
||||
cp .env.example .env
|
||||
# Edit .env with your backend API URL
|
||||
|
||||
# Start frontend dev server (port 5173)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Services will be available at:**
|
||||
- 🌐 **Frontend**: http://localhost:5173
|
||||
- 🔌 **Backend API**: http://localhost:3000
|
||||
- 📚 **API Documentation**: http://localhost:3000/api-docs.html
|
||||
- 🏥 **Health Check**: http://localhost:3000/api/health
|
||||
### Access the Application
|
||||
|
||||
### 2. Configure Google OAuth
|
||||
See [SETUP_GUIDE.md](SETUP_GUIDE.md) for detailed OAuth setup instructions.
|
||||
- **Frontend**: http://localhost:5173
|
||||
- **Backend API**: http://localhost:3000
|
||||
- **API Documentation**: http://localhost:3000/api (Swagger UI)
|
||||
|
||||
### 3. First Login
|
||||
- Visit http://localhost:5173
|
||||
- Click "Continue with Google"
|
||||
- First user becomes system administrator
|
||||
- Subsequent users need admin approval
|
||||
---
|
||||
|
||||
## 📚 API Documentation
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Interactive Documentation
|
||||
Visit **http://localhost:3000/api-docs.html** for:
|
||||
- 📖 Complete API reference with examples
|
||||
- 🧪 "Try it out" functionality for testing endpoints
|
||||
- 📋 Request/response schemas and validation rules
|
||||
- 🔐 Authentication requirements for each endpoint
|
||||
### Technology Stack
|
||||
|
||||
### Key API Categories
|
||||
- **🔐 Authentication**: `/auth/*` - OAuth, user management, role assignment
|
||||
- **👥 VIPs**: `/api/vips/*` - VIP profiles, scheduling, flight integration
|
||||
- **🚗 Drivers**: `/api/drivers/*` - Driver management, availability, conflicts
|
||||
- **✈️ Flights**: `/api/flights/*` - Flight tracking, real-time updates
|
||||
- **⚙️ Admin**: `/api/admin/*` - System settings, user approval
|
||||
**Backend**
|
||||
- **Framework**: NestJS 11 (TypeScript)
|
||||
- **Database**: PostgreSQL 16 with Prisma ORM 7.3
|
||||
- **Cache**: Redis 7
|
||||
- **Authentication**: Auth0 + Passport.js (JWT strategy)
|
||||
- **API**: REST with Swagger documentation
|
||||
|
||||
## 🛠️ Development
|
||||
**Frontend**
|
||||
- **Framework**: React 19 with TypeScript
|
||||
- **Build Tool**: Vite 7.2
|
||||
- **UI Library**: Material-UI (MUI) 7.3
|
||||
- **State Management**: React Query 5.9 (server) + Zustand 5.0 (client)
|
||||
- **Routing**: React Router 7.13
|
||||
- **Forms**: React Hook Form (planned)
|
||||
|
||||
**Infrastructure**
|
||||
- **Containerization**: Docker + Docker Compose
|
||||
- **Database Migrations**: Prisma Migrate
|
||||
- **Testing**: Playwright (E2E), Vitest (unit - planned)
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
- **Unified Activity Model**: Single ScheduleEvent entity for all activity types (transport, meals, meetings, events)
|
||||
- **Multi-VIP Support**: Activities can have multiple VIPs (`vipIds[]`) for ridesharing and group events
|
||||
- **Soft Deletes**: All entities use `deletedAt` field for audit trail preservation
|
||||
- **RBAC with CASL**: Role-based access control with isomorphic permission checking
|
||||
- **API Prefix**: All endpoints use `/api/v1` namespace
|
||||
|
||||
---
|
||||
|
||||
## 📋 Features
|
||||
|
||||
### Core Functionality
|
||||
|
||||
**VIP Management**
|
||||
- Create and manage VIP profiles with arrival details
|
||||
- Track arrival mode (FLIGHT, SELF_DRIVING, OTHER)
|
||||
- Flight information integration
|
||||
- Department assignment (OFFICE_OF_DEVELOPMENT for donors, ADMIN for BSA staff)
|
||||
- Complete activity timeline per VIP
|
||||
|
||||
**Activity Scheduling**
|
||||
- Unified model for all activity types:
|
||||
- 🚗 **TRANSPORT** - Airport pickups, venue shuttles, rideshares
|
||||
- 🍽️ **MEAL** - Breakfasts, lunches, dinners, receptions
|
||||
- 📅 **EVENT** - Ceremonies, tours, campfires, presentations
|
||||
- 🤝 **MEETING** - Donor meetings, briefings, private sessions
|
||||
- 🏨 **ACCOMMODATION** - Check-ins, check-outs, room assignments
|
||||
- Multi-VIP assignment (e.g., "3 VIPs sharing SUV to Campfire")
|
||||
- Driver and vehicle assignment with capacity tracking
|
||||
- Conflict detection and warnings
|
||||
- Status tracking (SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED)
|
||||
|
||||
**Search & Filtering**
|
||||
- **Real-time Search**: Instant filtering across title, location, description, VIP names, drivers, vehicles
|
||||
- **Type Filters**: Quick filter tabs (All, Transport, Meals, Events, Meetings, Accommodation)
|
||||
- **Sortable Columns**: Click to sort by Title, Type, VIPs, Start Time, or Status
|
||||
- **Combined Filtering**: Search + filters work together seamlessly
|
||||
|
||||
**Driver & Vehicle Management**
|
||||
- Driver profiles with contact info and department
|
||||
- Vehicle tracking with type and seat capacity
|
||||
- Real-time availability checking
|
||||
- Inline driver assignment from activity list
|
||||
|
||||
**Admin Tools**
|
||||
- One-click test data generation
|
||||
- Realistic BSA Jamboree scenarios (20 VIPs, 8 drivers, 8 vehicles, 300+ activities)
|
||||
- Balanced data: 50% OFFICE_OF_DEVELOPMENT, 50% ADMIN
|
||||
- Complete 3-day itineraries with 15 activities per VIP
|
||||
|
||||
### User Roles & Permissions
|
||||
|
||||
| Feature | Administrator | Coordinator | Driver |
|
||||
|---------|--------------|-------------|--------|
|
||||
| User Management | Full CRUD | View Only | None |
|
||||
| VIP Management | Full CRUD | Full CRUD | View Only |
|
||||
| Driver Management | Full CRUD | Full CRUD | View Only |
|
||||
| Activity Scheduling | Full CRUD | Full CRUD | View + Update Status |
|
||||
| Vehicle Management | Full CRUD | Full CRUD | View Only |
|
||||
| Flight Tracking | Full Access | Full Access | None |
|
||||
| Admin Tools | Full Access | Limited | None |
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database Schema
|
||||
|
||||
### Core Models
|
||||
|
||||
**User**
|
||||
```typescript
|
||||
- auth0Sub: string (unique)
|
||||
- email: string
|
||||
- name: string
|
||||
- role: ADMINISTRATOR | COORDINATOR | DRIVER
|
||||
- isApproved: boolean (manual approval required)
|
||||
- deletedAt: DateTime? (soft delete)
|
||||
```
|
||||
|
||||
**VIP**
|
||||
```typescript
|
||||
- name: string
|
||||
- organization: string?
|
||||
- department: OFFICE_OF_DEVELOPMENT | ADMIN
|
||||
- arrivalMode: FLIGHT | SELF_DRIVING | OTHER
|
||||
- expectedArrival: DateTime?
|
||||
- airportPickup: boolean
|
||||
- venueTransport: boolean
|
||||
- notes: string?
|
||||
- flights: Flight[] (relation)
|
||||
```
|
||||
|
||||
**ScheduleEvent** (Unified Activity Model)
|
||||
```typescript
|
||||
- title: string
|
||||
- type: TRANSPORT | MEAL | EVENT | MEETING | ACCOMMODATION
|
||||
- status: SCHEDULED | IN_PROGRESS | COMPLETED | CANCELLED
|
||||
- startTime: DateTime
|
||||
- endTime: DateTime
|
||||
- location: string?
|
||||
- pickupLocation: string? (transport only)
|
||||
- dropoffLocation: string? (transport only)
|
||||
- description: string?
|
||||
- notes: string?
|
||||
- vipIds: string[] (multi-VIP support)
|
||||
- driverId: string?
|
||||
- vehicleId: string?
|
||||
```
|
||||
|
||||
**Driver**
|
||||
```typescript
|
||||
- name: string
|
||||
- phone: string
|
||||
- department: OFFICE_OF_DEVELOPMENT | ADMIN
|
||||
- userId: string? (optional link to User)
|
||||
```
|
||||
|
||||
**Vehicle**
|
||||
```typescript
|
||||
- name: string
|
||||
- type: VAN | SUV | SEDAN | BUS | GOLF_CART | OTHER
|
||||
- licensePlate: string
|
||||
- seatCapacity: number
|
||||
- status: AVAILABLE | IN_USE | MAINTENANCE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication & Security
|
||||
|
||||
### Auth0 Integration
|
||||
|
||||
1. **Setup Auth0 Application**
|
||||
- Create Auth0 tenant at https://auth0.com
|
||||
- Create a "Regular Web Application"
|
||||
- Configure Allowed Callback URLs: `http://localhost:5173/callback`
|
||||
- Configure Allowed Logout URLs: `http://localhost:5173`
|
||||
- Configure Allowed Web Origins: `http://localhost:5173`
|
||||
|
||||
2. **Configure Backend** (`backend/.env`)
|
||||
```env
|
||||
AUTH0_DOMAIN=your-tenant.auth0.com
|
||||
AUTH0_AUDIENCE=https://your-tenant.auth0.com/api/v2/
|
||||
```
|
||||
|
||||
3. **Configure Frontend** (`frontend/.env`)
|
||||
```env
|
||||
VITE_AUTH0_DOMAIN=your-tenant.auth0.com
|
||||
VITE_AUTH0_CLIENT_ID=your_client_id
|
||||
VITE_AUTH0_AUDIENCE=https://your-tenant.auth0.com/api/v2/
|
||||
```
|
||||
|
||||
### User Approval Workflow
|
||||
|
||||
**New users must be manually approved:**
|
||||
1. User registers via Auth0
|
||||
2. User account created with `isApproved: false`
|
||||
3. Administrator manually approves user
|
||||
4. User gains access to system
|
||||
|
||||
⚠️ **First User Chicken-and-Egg Problem**: Use database seed script or manually set `isApproved: true` for first admin.
|
||||
|
||||
### Security Features
|
||||
|
||||
- **JWT Authentication**: Stateless token-based auth
|
||||
- **RBAC**: Role-based access control at route and UI levels
|
||||
- **API Guards**: NestJS guards enforce permissions on all endpoints
|
||||
- **Soft Deletes**: Audit trail preservation
|
||||
- **Input Validation**: DTO validation with class-validator
|
||||
- **SQL Injection Protection**: Prisma ORM parameterized queries
|
||||
|
||||
---
|
||||
|
||||
## 📡 API Endpoints
|
||||
|
||||
### Authentication
|
||||
```
|
||||
POST /api/v1/auth/login - Auth0 login
|
||||
POST /api/v1/auth/logout - Logout
|
||||
GET /api/v1/auth/me - Get current user
|
||||
```
|
||||
|
||||
### VIPs
|
||||
```
|
||||
GET /api/v1/vips - List all VIPs
|
||||
POST /api/v1/vips - Create VIP (Admin/Coordinator)
|
||||
GET /api/v1/vips/:id - Get VIP details
|
||||
PATCH /api/v1/vips/:id - Update VIP (Admin/Coordinator)
|
||||
DELETE /api/v1/vips/:id - Delete VIP (Admin/Coordinator)
|
||||
GET /api/v1/vips/:id/schedule - Get VIP's complete itinerary
|
||||
```
|
||||
|
||||
### Activities (ScheduleEvents)
|
||||
```
|
||||
GET /api/v1/events - List all activities
|
||||
POST /api/v1/events - Create activity (Admin/Coordinator)
|
||||
GET /api/v1/events/:id - Get activity details
|
||||
PATCH /api/v1/events/:id - Update activity (Admin/Coordinator/Driver)
|
||||
DELETE /api/v1/events/:id - Delete activity (Admin/Coordinator)
|
||||
PATCH /api/v1/events/:id/status - Update activity status (Driver allowed)
|
||||
POST /api/v1/events/:id/vips - Add VIPs to activity
|
||||
```
|
||||
|
||||
### Drivers
|
||||
```
|
||||
GET /api/v1/drivers - List all drivers
|
||||
POST /api/v1/drivers - Create driver (Admin/Coordinator)
|
||||
GET /api/v1/drivers/:id - Get driver details
|
||||
PATCH /api/v1/drivers/:id - Update driver (Admin/Coordinator)
|
||||
DELETE /api/v1/drivers/:id - Delete driver (Admin/Coordinator)
|
||||
```
|
||||
|
||||
### Vehicles
|
||||
```
|
||||
GET /api/v1/vehicles - List all vehicles
|
||||
POST /api/v1/vehicles - Create vehicle (Admin/Coordinator)
|
||||
GET /api/v1/vehicles/:id - Get vehicle details
|
||||
PATCH /api/v1/vehicles/:id - Update vehicle (Admin/Coordinator)
|
||||
DELETE /api/v1/vehicles/:id - Delete vehicle (Admin/Coordinator)
|
||||
```
|
||||
|
||||
### Users
|
||||
```
|
||||
GET /api/v1/users - List all users (Admin only)
|
||||
PATCH /api/v1/users/:id/approve - Approve user (Admin only)
|
||||
PATCH /api/v1/users/:id/role - Update user role (Admin only)
|
||||
DELETE /api/v1/users/:id - Delete user (Admin only)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### E2E Testing with Playwright
|
||||
|
||||
### Available Commands
|
||||
```bash
|
||||
# Start development environment
|
||||
make dev
|
||||
cd frontend
|
||||
|
||||
# View logs
|
||||
make logs
|
||||
# Install Playwright browsers (first time only)
|
||||
npx playwright install
|
||||
|
||||
# Stop all services
|
||||
make down
|
||||
# Run all E2E tests
|
||||
npx playwright test
|
||||
|
||||
# Rebuild containers
|
||||
make build
|
||||
# Run specific test file
|
||||
npx playwright test e2e/multi-vip-events.spec.ts
|
||||
|
||||
# Backend development
|
||||
cd backend && npm run dev
|
||||
# Run tests in UI mode (interactive)
|
||||
npx playwright test --ui
|
||||
|
||||
# Frontend development
|
||||
cd frontend && npm run dev
|
||||
# View test report
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
vip-coordinator/
|
||||
├── backend/ # Node.js API server
|
||||
│ ├── src/
|
||||
│ │ ├── routes/ # API route handlers
|
||||
│ │ ├── services/ # Business logic services
|
||||
│ │ ├── config/ # Configuration and auth
|
||||
│ │ └── index.ts # Main server file
|
||||
│ ├── package.json
|
||||
│ ├── tsconfig.json
|
||||
│ └── Dockerfile
|
||||
├── frontend/ # React frontend
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── pages/ # Page components
|
||||
│ │ ├── config/ # API configuration
|
||||
│ │ ├── App.tsx # Main app component
|
||||
│ │ └── main.tsx # Entry point
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.ts
|
||||
│ └── Dockerfile
|
||||
├── docker-compose.dev.yml # Development environment
|
||||
├── docker-compose.prod.yml # Production environment
|
||||
├── Makefile # Development commands
|
||||
├── SETUP_GUIDE.md # Detailed setup instructions
|
||||
└── README.md # This file
|
||||
```
|
||||
**Test Coverage**
|
||||
- ✅ Multi-VIP event creation and management
|
||||
- ✅ Search and filtering functionality
|
||||
- ✅ Driver assignment workflows
|
||||
- ✅ Authentication flows
|
||||
- ✅ Navigation and routing
|
||||
- ✅ API integration
|
||||
- ✅ Accessibility compliance
|
||||
- ✅ iPad/tablet UI responsiveness
|
||||
|
||||
## 🔐 User Roles & Permissions
|
||||
---
|
||||
|
||||
### Administrator
|
||||
- Full system access
|
||||
- User management and approval
|
||||
- System configuration
|
||||
- All VIP and driver operations
|
||||
## 🐛 Common Issues & Solutions
|
||||
|
||||
### Coordinator
|
||||
- VIP management (create, edit, delete)
|
||||
- Driver management
|
||||
- Schedule management
|
||||
- Flight tracking
|
||||
### Database Issues
|
||||
|
||||
### Driver
|
||||
- View assigned schedules
|
||||
- Update task status
|
||||
- Access driver dashboard
|
||||
|
||||
## 🌐 Deployment
|
||||
|
||||
### Development
|
||||
**Problem**: "Can't reach database server at localhost:5432"
|
||||
```bash
|
||||
make dev
|
||||
# Start PostgreSQL (if using Docker)
|
||||
docker-compose up -d postgres
|
||||
|
||||
# Or check if PostgreSQL is running locally
|
||||
sudo service postgresql status
|
||||
```
|
||||
|
||||
### Production
|
||||
**Problem**: "Migration failed" or "Schema drift detected"
|
||||
```bash
|
||||
# Build production images
|
||||
make build
|
||||
cd backend
|
||||
npx prisma migrate reset # ⚠️ Deletes all data
|
||||
npx prisma migrate deploy
|
||||
npx prisma db seed
|
||||
```
|
||||
|
||||
# Deploy with production configuration
|
||||
### Authentication Issues
|
||||
|
||||
**Problem**: "401 Unauthorized" on all API calls
|
||||
- Verify Auth0 domain and audience are correct in both backend and frontend `.env`
|
||||
- Check browser console for Auth0 errors
|
||||
- Verify JWT token is being sent in request headers
|
||||
- Ensure user is approved (`isApproved: true`)
|
||||
|
||||
**Problem**: Redirect loop on login
|
||||
- Check Auth0 callback URLs match frontend URL exactly
|
||||
- Clear browser cookies and local storage
|
||||
- Verify Auth0 client ID is correct
|
||||
|
||||
### Frontend Issues
|
||||
|
||||
**Problem**: "Cannot read property 'map' of undefined"
|
||||
- Data not loaded yet - add loading states
|
||||
- Check React Query cache invalidation
|
||||
- Verify API endpoint returns expected data structure
|
||||
|
||||
**Problem**: Search/sorting not working
|
||||
- Clear browser cache
|
||||
- Check console for JavaScript errors
|
||||
- Verify `filteredEvents` useMemo dependencies
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Production Checklist
|
||||
|
||||
- [ ] Set `NODE_ENV=production`
|
||||
- [ ] Use strong database passwords
|
||||
- [ ] Configure Auth0 for production domain
|
||||
- [ ] Enable HTTPS/SSL certificates
|
||||
- [ ] Set up automated database backups
|
||||
- [ ] Configure Redis persistence
|
||||
- [ ] Set up monitoring (e.g., Sentry, DataDog)
|
||||
- [ ] Configure CORS for production domain
|
||||
- [ ] Review and adjust rate limiting
|
||||
- [ ] Set up log aggregation
|
||||
- [ ] Configure CDN for frontend assets (optional)
|
||||
|
||||
### Docker Deployment (Production-Ready)
|
||||
|
||||
**Complete containerization with multi-stage builds, Nginx, and automated migrations.**
|
||||
|
||||
#### Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Create production environment file
|
||||
cp .env.production.example .env.production
|
||||
|
||||
# 2. Edit .env.production with your values
|
||||
# - Set strong POSTGRES_PASSWORD
|
||||
# - Configure Auth0 credentials
|
||||
# - Set AUTH0_CLIENT_ID for frontend
|
||||
|
||||
# 3. Build and start all services
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 4. Check service health
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# 5. View logs
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
```
|
||||
|
||||
#### What Gets Deployed
|
||||
|
||||
- **PostgreSQL 16** - Database with persistent volume
|
||||
- **Redis 7** - Caching layer with persistent volume
|
||||
- **Backend (NestJS)** - Optimized production build (~200MB)
|
||||
- Runs database migrations automatically on startup
|
||||
- Non-root user for security
|
||||
- Health checks enabled
|
||||
- **Frontend (Nginx)** - Static files served with Nginx (~45MB)
|
||||
- SPA routing configured
|
||||
- API requests proxied to backend
|
||||
- Gzip compression enabled
|
||||
- Security headers configured
|
||||
|
||||
#### First-Time Setup
|
||||
|
||||
**Auth0 Configuration:**
|
||||
1. Update callback URLs: `http://your-domain/callback`
|
||||
2. Update allowed web origins: `http://your-domain`
|
||||
3. Update logout URLs: `http://your-domain`
|
||||
|
||||
**Access Application:**
|
||||
- Frontend: `http://localhost` (or your domain)
|
||||
- Backend health: `http://localhost/api/v1/health`
|
||||
|
||||
#### Updating the Application
|
||||
|
||||
```bash
|
||||
# Pull latest code
|
||||
git pull
|
||||
|
||||
# Rebuild and restart
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
docker-compose -f docker-compose.prod.yml build --no-cache
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
See [SETUP_GUIDE.md](SETUP_GUIDE.md) for detailed environment variable configuration.
|
||||
#### Database Management
|
||||
|
||||
## 📋 Current Status
|
||||
```bash
|
||||
# View migration status
|
||||
docker-compose -f docker-compose.prod.yml exec backend npx prisma migrate status
|
||||
|
||||
### ✅ Implemented Features
|
||||
- Google OAuth authentication with JWT
|
||||
- Role-based access control
|
||||
- User approval workflow
|
||||
- VIP management with multi-flight support
|
||||
- Driver management and scheduling
|
||||
- Real-time flight tracking
|
||||
- Schedule conflict detection
|
||||
- Interactive API documentation
|
||||
- Docker containerization
|
||||
- PostgreSQL data persistence
|
||||
# Manually run migrations (not needed, runs automatically)
|
||||
docker-compose -f docker-compose.prod.yml exec backend npx prisma migrate deploy
|
||||
|
||||
### 🚧 Planned Features
|
||||
- [ ] Real-time GPS tracking for drivers
|
||||
- [ ] Push notifications for schedule changes
|
||||
- [ ] Mobile driver application
|
||||
- [ ] Advanced reporting and analytics
|
||||
- [ ] Google Sheets integration
|
||||
- [ ] Multi-tenant support
|
||||
- [ ] Advanced mapping features
|
||||
# Seed database with test data (optional)
|
||||
docker-compose -f docker-compose.prod.yml exec backend npx prisma db seed
|
||||
```
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
```bash
|
||||
# Check container status
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# View specific service logs
|
||||
docker-compose -f docker-compose.prod.yml logs backend
|
||||
docker-compose -f docker-compose.prod.yml logs frontend
|
||||
|
||||
# Restart specific service
|
||||
docker-compose -f docker-compose.prod.yml restart backend
|
||||
|
||||
# Complete reset (⚠️ DELETES ALL DATA)
|
||||
docker-compose -f docker-compose.prod.yml down -v
|
||||
docker volume rm vip-coordinator-postgres-data vip-coordinator-redis-data
|
||||
```
|
||||
|
||||
#### Production Enhancements
|
||||
|
||||
For production deployment, add:
|
||||
- **Reverse Proxy** (Caddy/Traefik) for SSL/TLS
|
||||
- **Automated Backups** for PostgreSQL volumes
|
||||
- **Monitoring** (Prometheus/Grafana)
|
||||
- **Log Aggregation** (ELK/Loki)
|
||||
|
||||
#### Image Sizes
|
||||
|
||||
- Backend: ~200-250MB (multi-stage build)
|
||||
- Frontend: ~45-50MB (nginx alpine)
|
||||
- Total deployment: <300MB (excluding database volumes)
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**Backend** (`backend/.env`)
|
||||
```env
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/vip_coordinator
|
||||
REDIS_URL=redis://localhost:6379
|
||||
AUTH0_DOMAIN=your-tenant.auth0.com
|
||||
AUTH0_AUDIENCE=https://your-tenant.auth0.com/api/v2/
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
**Frontend** (`frontend/.env`)
|
||||
```env
|
||||
VITE_API_URL=https://api.yourdomain.com
|
||||
VITE_AUTH0_DOMAIN=your-tenant.auth0.com
|
||||
VITE_AUTH0_CLIENT_ID=your_client_id
|
||||
VITE_AUTH0_AUDIENCE=https://your-tenant.auth0.com/api/v2/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Development Guide
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
vip-coordinator/
|
||||
├── backend/ # NestJS Backend
|
||||
│ ├── prisma/
|
||||
│ │ ├── schema.prisma # Database schema
|
||||
│ │ ├── migrations/ # Database migrations
|
||||
│ │ └── seed.ts # Test data seeding
|
||||
│ ├── src/
|
||||
│ │ ├── auth/ # Auth0 + JWT authentication
|
||||
│ │ ├── users/ # User management
|
||||
│ │ ├── vips/ # VIP management
|
||||
│ │ ├── drivers/ # Driver management
|
||||
│ │ ├── vehicles/ # Vehicle management
|
||||
│ │ ├── events/ # Activity scheduling (ScheduleEvent)
|
||||
│ │ ├── flights/ # Flight tracking
|
||||
│ │ └── common/ # Shared utilities, guards, decorators
|
||||
│ ├── Dockerfile # Multi-stage production build
|
||||
│ ├── docker-entrypoint.sh # Migration automation script
|
||||
│ ├── .dockerignore # Docker build exclusions
|
||||
│ └── package.json
|
||||
│
|
||||
├── frontend/ # React Frontend
|
||||
│ ├── e2e/ # Playwright E2E tests
|
||||
│ ├── src/
|
||||
│ │ ├── pages/ # Page components
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── contexts/ # React contexts (Auth)
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── lib/ # Utilities, API client
|
||||
│ │ └── types/ # TypeScript types
|
||||
│ ├── Dockerfile # Multi-stage build with Nginx
|
||||
│ ├── nginx.conf # Nginx server configuration
|
||||
│ ├── .dockerignore # Docker build exclusions
|
||||
│ ├── playwright.config.ts # Playwright configuration
|
||||
│ └── package.json
|
||||
│
|
||||
├── docker-compose.yml # Development environment (DB only)
|
||||
├── docker-compose.prod.yml # Production deployment (full stack)
|
||||
├── .env.production.example # Production environment template
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
|
||||
```bash
|
||||
# Create a new migration
|
||||
cd backend
|
||||
npx prisma migrate dev --name description_of_change
|
||||
|
||||
# Apply migrations to production
|
||||
npx prisma migrate deploy
|
||||
|
||||
# Reset database (⚠️ deletes all data)
|
||||
npx prisma migrate reset
|
||||
|
||||
# View migration status
|
||||
npx prisma migrate status
|
||||
```
|
||||
|
||||
### Adding a New Feature
|
||||
|
||||
1. **Backend**
|
||||
```bash
|
||||
cd backend
|
||||
nest g module feature-name
|
||||
nest g service feature-name
|
||||
nest g controller feature-name
|
||||
```
|
||||
|
||||
2. **Update Prisma Schema** (if needed)
|
||||
```prisma
|
||||
model NewEntity {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime? // Soft delete
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create Migration**
|
||||
```bash
|
||||
npx prisma migrate dev --name add_new_entity
|
||||
```
|
||||
|
||||
4. **Frontend**
|
||||
- Create page in `frontend/src/pages/`
|
||||
- Add route in `frontend/src/App.tsx`
|
||||
- Create API service methods in `frontend/src/lib/api.ts`
|
||||
- Add navigation link in `frontend/src/components/Layout.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
||||
3. Make your changes and test thoroughly
|
||||
4. Commit your changes: `git commit -m 'Add amazing feature'`
|
||||
5. Push to the branch: `git push origin feature/amazing-feature`
|
||||
6. Submit a pull request
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Make your changes
|
||||
4. Write/update tests if applicable
|
||||
5. Commit with descriptive messages
|
||||
6. Push to your branch
|
||||
7. Create a Pull Request
|
||||
|
||||
## 📄 License
|
||||
### Commit Message Format
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
```
|
||||
<type>: <subject>
|
||||
|
||||
## 🆘 Support
|
||||
<body>
|
||||
|
||||
- 📖 **Documentation**: Check [SETUP_GUIDE.md](SETUP_GUIDE.md) for detailed setup
|
||||
- 🔧 **API Reference**: Visit http://localhost:3000/api-docs.html
|
||||
- 🐛 **Issues**: Report bugs and request features via GitHub issues
|
||||
- 💬 **Discussions**: Use GitHub discussions for questions and ideas
|
||||
Co-Authored-By: Your Name <your.email@example.com>
|
||||
```
|
||||
|
||||
**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
||||
|
||||
---
|
||||
|
||||
**VIP Coordinator** - Streamlining VIP logistics with modern web technology.
|
||||
## 📖 Additional Documentation
|
||||
|
||||
- **CLAUDE.md** - Comprehensive project context for AI assistants
|
||||
- **PLAYWRIGHT_GUIDE.md** - E2E testing guide
|
||||
- **NAVIGATION_UX_IMPROVEMENTS.md** - UI/UX enhancement notes
|
||||
- **KEYCLOAK_SETUP.md** - Alternative auth setup (archived)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Changelog
|
||||
|
||||
### Latest (v2.0.0) - 2026-01-31
|
||||
|
||||
**Major Changes**
|
||||
- ✨ Unified activity system (merged Event/EventTemplate → ScheduleEvent)
|
||||
- ✨ Multi-VIP support (`vipIds[]` array for ridesharing)
|
||||
- ✨ Advanced search with real-time filtering
|
||||
- ✨ Sortable data tables (Title, Type, VIPs, Start Time, Status)
|
||||
- ✨ Balanced BSA-relevant test data
|
||||
- 🔧 Renamed "Schedule" → "Activities" throughout app
|
||||
- 🗄️ Database schema overhaul (3 new migrations)
|
||||
- 🧪 Playwright E2E test suite added
|
||||
- 📚 Complete documentation rewrite
|
||||
|
||||
**Breaking Changes**
|
||||
- API: `vipId` → `vipIds[]` in all event endpoints
|
||||
- Database: Event/EventTemplate tables dropped
|
||||
- Migration required: `npx prisma migrate deploy`
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is proprietary software developed for BSA Jamboree operations.
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
**For Issues:**
|
||||
1. Check this README's troubleshooting section
|
||||
2. Review logs: `docker-compose logs -f`
|
||||
3. Check CLAUDE.md for detailed context
|
||||
4. Create an issue in Gitea with:
|
||||
- Steps to reproduce
|
||||
- Error messages/logs
|
||||
- Environment details
|
||||
- Screenshots if applicable
|
||||
|
||||
**For Questions:**
|
||||
- Review documentation files in repository root
|
||||
- Check API documentation at `/api` endpoint
|
||||
- Review Playwright test examples in `frontend/e2e/`
|
||||
|
||||
---
|
||||
|
||||
**Built for BSA Jamboree Operations** 🏕️
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
# 🌐 Reverse Proxy OAuth Setup Guide
|
||||
|
||||
## Your Current Setup
|
||||
- **Internet** → **Router (ports 80/443)** → **Reverse Proxy** → **Frontend (port 5173)**
|
||||
- **Backend (port 3000)** is only accessible locally
|
||||
- **OAuth callback fails** because Google can't reach the backend
|
||||
|
||||
## The Problem
|
||||
Google OAuth needs to redirect to your **backend** (`/auth/google/callback`), but your reverse proxy only forwards to the frontend. The backend port 3000 isn't exposed to the internet.
|
||||
|
||||
## Solution: Configure Reverse Proxy for Both Frontend and Backend
|
||||
|
||||
### Option 1: Single Domain with Path-Based Routing (Recommended)
|
||||
|
||||
Configure your reverse proxy to route both frontend and backend on the same domain:
|
||||
|
||||
```nginx
|
||||
# Example Nginx configuration
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name bsa.madeamess.online;
|
||||
|
||||
# Frontend routes (everything except /auth and /api)
|
||||
location / {
|
||||
proxy_pass http://localhost:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Backend API routes
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Backend auth routes (CRITICAL for OAuth)
|
||||
location /auth/ {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Subdomain Routing
|
||||
|
||||
If you prefer separate subdomains:
|
||||
|
||||
```nginx
|
||||
# Frontend
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name bsa.madeamess.online;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5173;
|
||||
# ... headers
|
||||
}
|
||||
}
|
||||
|
||||
# Backend API
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name api.bsa.madeamess.online;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
# ... headers
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Update Environment Variables
|
||||
|
||||
### For Option 1 (Path-based - Recommended):
|
||||
|
||||
```bash
|
||||
# backend/.env
|
||||
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
GOOGLE_REDIRECT_URI=https://bsa.madeamess.online/auth/google/callback
|
||||
FRONTEND_URL=https://bsa.madeamess.online
|
||||
```
|
||||
|
||||
### For Option 2 (Subdomain):
|
||||
|
||||
```bash
|
||||
# backend/.env
|
||||
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
|
||||
FRONTEND_URL=https://bsa.madeamess.online
|
||||
```
|
||||
|
||||
## Update Google Cloud Console
|
||||
|
||||
### For Option 1 (Path-based):
|
||||
|
||||
**Authorized JavaScript origins:**
|
||||
```
|
||||
https://bsa.madeamess.online
|
||||
```
|
||||
|
||||
**Authorized redirect URIs:**
|
||||
```
|
||||
https://bsa.madeamess.online/auth/google/callback
|
||||
```
|
||||
|
||||
### For Option 2 (Subdomain):
|
||||
|
||||
**Authorized JavaScript origins:**
|
||||
```
|
||||
https://bsa.madeamess.online
|
||||
https://api.bsa.madeamess.online
|
||||
```
|
||||
|
||||
**Authorized redirect URIs:**
|
||||
```
|
||||
https://api.bsa.madeamess.online/auth/google/callback
|
||||
```
|
||||
|
||||
## Frontend Configuration Update
|
||||
|
||||
If using Option 2 (subdomain), update your frontend to call the API subdomain:
|
||||
|
||||
```javascript
|
||||
// In your frontend code, change API calls from:
|
||||
fetch('/auth/google/url')
|
||||
|
||||
// To:
|
||||
fetch('https://api.bsa.madeamess.online/auth/google/url')
|
||||
```
|
||||
|
||||
## Testing Your Setup
|
||||
|
||||
### 1. Test Backend Accessibility
|
||||
|
||||
```bash
|
||||
# Should work from internet
|
||||
curl https://bsa.madeamess.online/auth/setup
|
||||
# or for subdomain:
|
||||
curl https://api.bsa.madeamess.online/auth/setup
|
||||
```
|
||||
|
||||
### 2. Test OAuth URL Generation
|
||||
|
||||
```bash
|
||||
curl https://bsa.madeamess.online/auth/google/url
|
||||
# Should return a Google OAuth URL
|
||||
```
|
||||
|
||||
### 3. Test Complete Flow
|
||||
|
||||
1. Visit `https://bsa.madeamess.online`
|
||||
2. Click "Continue with Google"
|
||||
3. Complete Google login
|
||||
4. Should redirect back and authenticate
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: "Invalid redirect URI"
|
||||
- **Cause**: Google Console redirect URI doesn't match exactly
|
||||
- **Fix**: Ensure exact match including `https://` and no trailing slash
|
||||
|
||||
### Issue: "OAuth not configured"
|
||||
- **Cause**: Backend environment variables not updated
|
||||
- **Fix**: Update `.env` file and restart containers
|
||||
|
||||
### Issue: Frontend can't reach backend
|
||||
- **Cause**: Reverse proxy not configured for `/auth` and `/api` routes
|
||||
- **Fix**: Add backend routing to your reverse proxy config
|
||||
|
||||
### Issue: CORS errors
|
||||
- **Cause**: Frontend and backend on different origins
|
||||
- **Fix**: Update CORS configuration in backend:
|
||||
|
||||
```javascript
|
||||
// In backend/src/index.ts
|
||||
app.use(cors({
|
||||
origin: [
|
||||
'https://bsa.madeamess.online',
|
||||
'http://localhost:5173' // for local development
|
||||
],
|
||||
credentials: true
|
||||
}));
|
||||
```
|
||||
|
||||
## Recommended: Path-Based Routing
|
||||
|
||||
I recommend **Option 1 (path-based routing)** because:
|
||||
- ✅ Single domain simplifies CORS
|
||||
- ✅ Easier SSL certificate management
|
||||
- ✅ Simpler frontend configuration
|
||||
- ✅ Better for SEO and user experience
|
||||
|
||||
## Quick Setup Commands
|
||||
|
||||
```bash
|
||||
# 1. Update environment variables
|
||||
cd /home/kyle/Desktop/vip-coordinator
|
||||
# Edit backend/.env with your domain
|
||||
|
||||
# 2. Restart containers
|
||||
docker-compose -f docker-compose.dev.yml restart
|
||||
|
||||
# 3. Test the setup
|
||||
curl https://bsa.madeamess.online/auth/setup
|
||||
```
|
||||
|
||||
Your OAuth should work once you configure your reverse proxy to forward `/auth` and `/api` routes to the backend (port 3000)!
|
||||
@@ -1,300 +0,0 @@
|
||||
# Role-Based Access Control (RBAC) System
|
||||
|
||||
## Overview
|
||||
|
||||
The VIP Coordinator application implements a comprehensive role-based access control system with three distinct user roles, each with specific permissions and access levels.
|
||||
|
||||
## User Roles
|
||||
|
||||
### 1. System Administrator (`administrator`)
|
||||
**Highest privilege level - Full system access**
|
||||
|
||||
#### Permissions:
|
||||
- ✅ **User Management**: Create, read, update, delete users
|
||||
- ✅ **Role Management**: Assign and modify user roles
|
||||
- ✅ **VIP Management**: Full CRUD operations on VIP records
|
||||
- ✅ **Driver Management**: Full CRUD operations on driver records
|
||||
- ✅ **Schedule Management**: Full CRUD operations on schedules
|
||||
- ✅ **System Settings**: Access to admin panel and API configurations
|
||||
- ✅ **Flight Tracking**: Access to all flight tracking features
|
||||
- ✅ **Reports & Analytics**: Access to all system reports
|
||||
|
||||
#### API Endpoints Access:
|
||||
```
|
||||
POST /auth/users ✅ Admin only
|
||||
GET /auth/users ✅ Admin only
|
||||
PATCH /auth/users/:email/role ✅ Admin only
|
||||
DELETE /auth/users/:email ✅ Admin only
|
||||
|
||||
POST /api/vips ✅ Admin + Coordinator
|
||||
GET /api/vips ✅ All authenticated users
|
||||
PUT /api/vips/:id ✅ Admin + Coordinator
|
||||
DELETE /api/vips/:id ✅ Admin + Coordinator
|
||||
|
||||
POST /api/drivers ✅ Admin + Coordinator
|
||||
GET /api/drivers ✅ All authenticated users
|
||||
PUT /api/drivers/:id ✅ Admin + Coordinator
|
||||
DELETE /api/drivers/:id ✅ Admin + Coordinator
|
||||
|
||||
POST /api/vips/:vipId/schedule ✅ Admin + Coordinator
|
||||
GET /api/vips/:vipId/schedule ✅ All authenticated users
|
||||
PUT /api/vips/:vipId/schedule/:id ✅ Admin + Coordinator
|
||||
PATCH /api/vips/:vipId/schedule/:id/status ✅ All authenticated users
|
||||
DELETE /api/vips/:vipId/schedule/:id ✅ Admin + Coordinator
|
||||
```
|
||||
|
||||
### 2. Coordinator (`coordinator`)
|
||||
**Standard operational access - Can manage VIPs, drivers, and schedules**
|
||||
|
||||
#### Permissions:
|
||||
- ❌ **User Management**: Cannot manage users or roles
|
||||
- ✅ **VIP Management**: Full CRUD operations on VIP records
|
||||
- ✅ **Driver Management**: Full CRUD operations on driver records
|
||||
- ✅ **Schedule Management**: Full CRUD operations on schedules
|
||||
- ❌ **System Settings**: No access to admin panel
|
||||
- ✅ **Flight Tracking**: Access to flight tracking features
|
||||
- ✅ **Driver Availability**: Can check driver conflicts and availability
|
||||
- ✅ **Status Updates**: Can update event statuses
|
||||
|
||||
#### Typical Use Cases:
|
||||
- Managing VIP arrivals and departures
|
||||
- Assigning drivers to VIPs
|
||||
- Creating and updating schedules
|
||||
- Monitoring flight statuses
|
||||
- Coordinating transportation logistics
|
||||
|
||||
### 3. Driver (`driver`)
|
||||
**Limited access - Can view assigned schedules and update status**
|
||||
|
||||
#### Permissions:
|
||||
- ❌ **User Management**: Cannot manage users
|
||||
- ❌ **VIP Management**: Cannot create/edit/delete VIPs
|
||||
- ❌ **Driver Management**: Cannot manage other drivers
|
||||
- ❌ **Schedule Creation**: Cannot create or delete schedules
|
||||
- ✅ **View Schedules**: Can view VIP schedules and assigned events
|
||||
- ✅ **Status Updates**: Can update status of assigned events
|
||||
- ✅ **Personal Schedule**: Can view their own complete schedule
|
||||
- ❌ **System Settings**: No access to admin features
|
||||
|
||||
#### API Endpoints Access:
|
||||
```
|
||||
GET /api/vips ✅ View only
|
||||
GET /api/drivers ✅ View only
|
||||
GET /api/vips/:vipId/schedule ✅ View only
|
||||
PATCH /api/vips/:vipId/schedule/:id/status ✅ Can update status
|
||||
GET /api/drivers/:driverId/schedule ✅ Own schedule only
|
||||
```
|
||||
|
||||
#### Typical Use Cases:
|
||||
- Viewing assigned VIP transportation schedules
|
||||
- Updating event status (en route, completed, delayed)
|
||||
- Checking personal daily/weekly schedule
|
||||
- Viewing VIP contact information and notes
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### 1. Google OAuth Integration
|
||||
- Users authenticate via Google OAuth 2.0
|
||||
- First user automatically becomes `administrator`
|
||||
- Subsequent users default to `coordinator` role
|
||||
- Administrators can change user roles after authentication
|
||||
|
||||
### 2. JWT Token System
|
||||
- Secure JWT tokens issued after successful authentication
|
||||
- Tokens include user role information
|
||||
- Middleware validates tokens and role permissions on each request
|
||||
|
||||
### 3. Role Assignment
|
||||
```typescript
|
||||
// First user becomes admin
|
||||
const userCount = await databaseService.getUserCount();
|
||||
const role = userCount === 0 ? 'administrator' : 'coordinator';
|
||||
```
|
||||
|
||||
## Security Implementation
|
||||
|
||||
### Middleware Protection
|
||||
```typescript
|
||||
// Authentication required
|
||||
app.get('/api/vips', requireAuth, async (req, res) => { ... });
|
||||
|
||||
// Role-based access
|
||||
app.post('/api/vips', requireAuth, requireRole(['coordinator', 'administrator']),
|
||||
async (req, res) => { ... });
|
||||
|
||||
// Admin only
|
||||
app.get('/auth/users', requireAuth, requireRole(['administrator']),
|
||||
async (req, res) => { ... });
|
||||
```
|
||||
|
||||
### Frontend Role Checking
|
||||
```typescript
|
||||
// User Management component
|
||||
if (currentUser?.role !== 'administrator') {
|
||||
return (
|
||||
<div className="p-6 bg-red-50 border border-red-200 rounded-lg">
|
||||
<h2 className="text-xl font-semibold text-red-800 mb-2">Access Denied</h2>
|
||||
<p className="text-red-600">You need administrator privileges to access user management.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Users Table
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
picture TEXT,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'coordinator',
|
||||
provider VARCHAR(50) NOT NULL DEFAULT 'google',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_sign_in_at TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
```
|
||||
|
||||
## Role Transition Guidelines
|
||||
|
||||
### Promoting Users
|
||||
1. **Coordinator → Administrator**
|
||||
- Grants full system access
|
||||
- Can manage other users
|
||||
- Access to system settings
|
||||
- Should be limited to trusted personnel
|
||||
|
||||
2. **Driver → Coordinator**
|
||||
- Grants VIP and schedule management
|
||||
- Can assign other drivers
|
||||
- Suitable for supervisory roles
|
||||
|
||||
### Demoting Users
|
||||
1. **Administrator → Coordinator**
|
||||
- Removes user management access
|
||||
- Retains operational capabilities
|
||||
- Cannot access system settings
|
||||
|
||||
2. **Coordinator → Driver**
|
||||
- Removes management capabilities
|
||||
- Retains view and status update access
|
||||
- Suitable for field personnel
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Principle of Least Privilege
|
||||
- Users should have minimum permissions necessary for their role
|
||||
- Regular review of user roles and permissions
|
||||
- Temporary elevation should be avoided
|
||||
|
||||
### 2. Role Assignment Strategy
|
||||
- **Administrators**: IT staff, senior management (limit to 2-3 users)
|
||||
- **Coordinators**: Operations staff, event coordinators (primary users)
|
||||
- **Drivers**: Field personnel, transportation staff
|
||||
|
||||
### 3. Security Considerations
|
||||
- Regular audit of user access logs
|
||||
- Monitor for privilege escalation attempts
|
||||
- Implement session timeouts for sensitive operations
|
||||
- Use HTTPS for all authentication flows
|
||||
|
||||
### 4. Emergency Access
|
||||
- Maintain at least one administrator account
|
||||
- Document emergency access procedures
|
||||
- Consider backup authentication methods
|
||||
|
||||
## API Security Features
|
||||
|
||||
### 1. Token Validation
|
||||
```typescript
|
||||
export function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const user = verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
(req as any).user = user;
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Role Validation
|
||||
```typescript
|
||||
export function requireRole(roles: string[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const user = (req as any).user;
|
||||
|
||||
if (!user || !roles.includes(user.role)) {
|
||||
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring and Auditing
|
||||
|
||||
### 1. User Activity Logging
|
||||
- Track user login/logout events
|
||||
- Log role changes and who made them
|
||||
- Monitor sensitive operations (user deletion, role changes)
|
||||
|
||||
### 2. Access Attempt Monitoring
|
||||
- Failed authentication attempts
|
||||
- Unauthorized access attempts
|
||||
- Privilege escalation attempts
|
||||
|
||||
### 3. Regular Security Reviews
|
||||
- Quarterly review of user roles
|
||||
- Annual security audit
|
||||
- Regular password/token rotation
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### 1. Granular Permissions
|
||||
- Department-based access control
|
||||
- Resource-specific permissions
|
||||
- Time-based access restrictions
|
||||
|
||||
### 2. Advanced Security Features
|
||||
- Multi-factor authentication
|
||||
- IP-based access restrictions
|
||||
- Session management improvements
|
||||
|
||||
### 3. Audit Trail
|
||||
- Comprehensive activity logging
|
||||
- Change history tracking
|
||||
- Compliance reporting
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Feature | Administrator | Coordinator | Driver |
|
||||
|---------|--------------|-------------|--------|
|
||||
| User Management | ✅ | ❌ | ❌ |
|
||||
| VIP CRUD | ✅ | ✅ | ❌ |
|
||||
| Driver CRUD | ✅ | ✅ | ❌ |
|
||||
| Schedule CRUD | ✅ | ✅ | ❌ |
|
||||
| Status Updates | ✅ | ✅ | ✅ |
|
||||
| View Data | ✅ | ✅ | ✅ |
|
||||
| System Settings | ✅ | ❌ | ❌ |
|
||||
| Flight Tracking | ✅ | ✅ | ❌ |
|
||||
|
||||
**Last Updated**: June 2, 2025
|
||||
**Version**: 1.0
|
||||
314
SETUP_GUIDE.md
314
SETUP_GUIDE.md
@@ -1,314 +0,0 @@
|
||||
# VIP Coordinator Setup Guide
|
||||
|
||||
A comprehensive guide to set up and run the VIP Coordinator system.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Docker and Docker Compose
|
||||
- Google Cloud Console account (for OAuth)
|
||||
|
||||
### 1. Clone and Start
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd vip-coordinator
|
||||
make dev
|
||||
```
|
||||
|
||||
The application will be available at:
|
||||
- **Frontend**: http://localhost:5173
|
||||
- **Backend API**: http://localhost:3000
|
||||
- **API Documentation**: http://localhost:3000/api-docs.html
|
||||
|
||||
### 2. Google OAuth Setup (Required)
|
||||
|
||||
1. **Create Google Cloud Project**:
|
||||
- Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
- Create a new project or select existing one
|
||||
|
||||
2. **Enable Google+ API**:
|
||||
- Navigate to "APIs & Services" > "Library"
|
||||
- Search for "Google+ API" and enable it
|
||||
|
||||
3. **Create OAuth Credentials**:
|
||||
- Go to "APIs & Services" > "Credentials"
|
||||
- Click "Create Credentials" > "OAuth 2.0 Client IDs"
|
||||
- Application type: "Web application"
|
||||
- Authorized redirect URIs: `http://localhost:3000/auth/google/callback`
|
||||
|
||||
4. **Configure Environment**:
|
||||
```bash
|
||||
# Copy the example environment file
|
||||
cp backend/.env.example backend/.env
|
||||
|
||||
# Edit backend/.env and add your Google OAuth credentials:
|
||||
GOOGLE_CLIENT_ID=your-client-id-here
|
||||
GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||
```
|
||||
|
||||
5. **Restart the Application**:
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
|
||||
### 3. First Login
|
||||
- Visit http://localhost:5173
|
||||
- Click "Continue with Google"
|
||||
- The first user to log in becomes the system administrator
|
||||
- Subsequent users need administrator approval
|
||||
|
||||
## 🏗️ Architecture Overview
|
||||
|
||||
### Authentication System
|
||||
- **JWT-based authentication** with Google OAuth
|
||||
- **Role-based access control**: Administrator, Coordinator, Driver
|
||||
- **User approval system** for new registrations
|
||||
- **Simple setup** - no complex OAuth configurations needed
|
||||
|
||||
### Database
|
||||
- **PostgreSQL** for persistent data storage
|
||||
- **Automatic schema initialization** on first run
|
||||
- **User management** with approval workflows
|
||||
- **VIP and driver data** with scheduling
|
||||
|
||||
### API Structure
|
||||
- **RESTful API** with comprehensive endpoints
|
||||
- **OpenAPI/Swagger documentation** at `/api-docs.html`
|
||||
- **Role-based endpoint protection**
|
||||
- **Real-time flight tracking** integration
|
||||
|
||||
## 📋 Features
|
||||
|
||||
### Current Features
|
||||
- ✅ **User Management**: Google OAuth with role-based access
|
||||
- ✅ **VIP Management**: Create, edit, track VIPs with flight information
|
||||
- ✅ **Driver Coordination**: Manage drivers and assignments
|
||||
- ✅ **Flight Tracking**: Real-time flight status updates
|
||||
- ✅ **Schedule Management**: Event scheduling with conflict detection
|
||||
- ✅ **Department Support**: Office of Development and Admin departments
|
||||
- ✅ **API Documentation**: Interactive Swagger UI
|
||||
|
||||
### User Roles
|
||||
- **Administrator**: Full system access, user management
|
||||
- **Coordinator**: VIP and driver management, scheduling
|
||||
- **Driver**: View assigned schedules (planned)
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=postgresql://vip_user:vip_password@db:5432/vip_coordinator
|
||||
|
||||
# Authentication
|
||||
GOOGLE_CLIENT_ID=your-google-client-id
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
JWT_SECRET=your-jwt-secret-key
|
||||
|
||||
# External APIs (Optional)
|
||||
AVIATIONSTACK_API_KEY=your-aviationstack-key
|
||||
|
||||
# Application
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
### Docker Services
|
||||
- **Frontend**: React + Vite development server
|
||||
- **Backend**: Node.js + Express API server
|
||||
- **Database**: PostgreSQL with automatic initialization
|
||||
- **Redis**: Caching and real-time updates
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Available Commands
|
||||
```bash
|
||||
# Start development environment
|
||||
make dev
|
||||
|
||||
# View logs
|
||||
make logs
|
||||
|
||||
# Stop all services
|
||||
make down
|
||||
|
||||
# Rebuild containers
|
||||
make build
|
||||
|
||||
# Backend only
|
||||
cd backend && npm run dev
|
||||
|
||||
# Frontend only
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
### API Testing
|
||||
- **Interactive Documentation**: http://localhost:3000/api-docs.html
|
||||
- **Health Check**: http://localhost:3000/api/health
|
||||
- **Authentication Test**: Use the "Try it out" feature in Swagger UI
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
### Authentication Flow
|
||||
1. User clicks "Continue with Google"
|
||||
2. Redirected to Google OAuth
|
||||
3. Google redirects back with authorization code
|
||||
4. Backend exchanges code for user info
|
||||
5. JWT token generated and returned
|
||||
6. Frontend stores token for API requests
|
||||
|
||||
### API Protection
|
||||
- All API endpoints require valid JWT token
|
||||
- Role-based access control on sensitive operations
|
||||
- User approval system for new registrations
|
||||
|
||||
## 📚 API Documentation
|
||||
|
||||
### Key Endpoints
|
||||
- **Authentication**: `/auth/*` - OAuth and user management
|
||||
- **VIPs**: `/api/vips/*` - VIP management and scheduling
|
||||
- **Drivers**: `/api/drivers/*` - Driver management and availability
|
||||
- **Flights**: `/api/flights/*` - Flight tracking and information
|
||||
- **Admin**: `/api/admin/*` - System administration
|
||||
|
||||
### Interactive Documentation
|
||||
Visit http://localhost:3000/api-docs.html for:
|
||||
- Complete API reference
|
||||
- Request/response examples
|
||||
- "Try it out" functionality
|
||||
- Schema definitions
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**OAuth Not Working**:
|
||||
- Verify Google Client ID and Secret in `.env`
|
||||
- Check redirect URI in Google Console matches exactly
|
||||
- Ensure Google+ API is enabled
|
||||
|
||||
**Database Connection Error**:
|
||||
- Verify Docker containers are running: `docker ps`
|
||||
- Check database logs: `docker-compose logs db`
|
||||
- Restart services: `make down && make dev`
|
||||
|
||||
**Frontend Can't Connect to Backend**:
|
||||
- Verify backend is running on port 3000
|
||||
- Check CORS configuration in backend
|
||||
- Ensure FRONTEND_URL is set correctly
|
||||
|
||||
### Getting Help
|
||||
1. Check the interactive API documentation
|
||||
2. Review Docker container logs
|
||||
3. Verify environment configuration
|
||||
4. Test with the health check endpoint
|
||||
|
||||
## 🔄 Production Deployment
|
||||
|
||||
### Prerequisites for Production
|
||||
|
||||
1. **Domain Setup**: Ensure your domains are configured:
|
||||
- Frontend: `https://bsa.madeamess.online`
|
||||
- API: `https://api.bsa.madeamess.online`
|
||||
|
||||
2. **SSL Certificates**: Configure SSL/TLS certificates for your domains
|
||||
|
||||
3. **Environment Configuration**: Copy and configure production environment:
|
||||
```bash
|
||||
cp .env.production .env.prod
|
||||
# Edit .env.prod with your secure values
|
||||
```
|
||||
|
||||
### Production Deployment Steps
|
||||
|
||||
1. **Configure Environment Variables**:
|
||||
```bash
|
||||
# Edit .env.prod with secure values:
|
||||
# - Change DB_PASSWORD to a strong password
|
||||
# - Generate new JWT_SECRET and SESSION_SECRET
|
||||
# - Update ADMIN_PASSWORD
|
||||
# - Set your AVIATIONSTACK_API_KEY
|
||||
```
|
||||
|
||||
2. **Deploy with Production Configuration**:
|
||||
```bash
|
||||
# Load production environment
|
||||
export $(cat .env.prod | xargs)
|
||||
|
||||
# Build and start production containers
|
||||
docker-compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
3. **Verify Deployment**:
|
||||
```bash
|
||||
# Check container status
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# View logs
|
||||
docker-compose -f docker-compose.prod.yml logs
|
||||
```
|
||||
|
||||
### Production vs Development Differences
|
||||
|
||||
| Feature | Development | Production |
|
||||
|---------|-------------|------------|
|
||||
| Build Target | `development` | `production` |
|
||||
| Source Code | Volume mounted (hot reload) | Built into image |
|
||||
| Database Password | Hardcoded `changeme` | Environment variable |
|
||||
| Frontend Server | Vite dev server (port 5173) | Nginx (port 80) |
|
||||
| API URL | `http://localhost:3000/api` | `https://api.bsa.madeamess.online/api` |
|
||||
| SSL/HTTPS | Not configured | Required |
|
||||
| Restart Policy | Manual | `unless-stopped` |
|
||||
|
||||
### Production Environment Variables
|
||||
|
||||
```bash
|
||||
# Database Configuration
|
||||
DB_PASSWORD=your-secure-database-password-here
|
||||
|
||||
# Domain Configuration
|
||||
DOMAIN=bsa.madeamess.online
|
||||
VITE_API_URL=https://api.bsa.madeamess.online/api
|
||||
|
||||
# Authentication Configuration (Generate new secure keys)
|
||||
JWT_SECRET=your-super-secure-jwt-secret-key-change-in-production-12345
|
||||
SESSION_SECRET=your-super-secure-session-secret-change-in-production-67890
|
||||
|
||||
# Google OAuth Configuration
|
||||
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=https://bsa.madeamess.online
|
||||
|
||||
# Flight API Configuration
|
||||
AVIATIONSTACK_API_KEY=your-aviationstack-api-key
|
||||
|
||||
# Admin Configuration
|
||||
ADMIN_PASSWORD=your-secure-admin-password
|
||||
|
||||
# Port Configuration
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
### Production-Specific Troubleshooting
|
||||
|
||||
**SSL Certificate errors**: Ensure certificates are properly configured
|
||||
**Domain resolution**: Verify DNS settings for your domains
|
||||
**Environment variables**: Check that all required variables are set in `.env.prod`
|
||||
**Firewall**: Ensure ports 80, 443, 3000 are accessible
|
||||
|
||||
### Production Logs
|
||||
|
||||
```bash
|
||||
# View production container logs
|
||||
docker-compose -f docker-compose.prod.yml logs backend
|
||||
docker-compose -f docker-compose.prod.yml logs frontend
|
||||
docker-compose -f docker-compose.prod.yml logs db
|
||||
|
||||
# Follow logs in real-time
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
```
|
||||
|
||||
This setup guide reflects the current simple, effective architecture of the VIP Coordinator system with production-ready deployment capabilities.
|
||||
@@ -1,159 +0,0 @@
|
||||
# Simple OAuth2 Setup Guide
|
||||
|
||||
## ✅ What's Working Now
|
||||
|
||||
The VIP Coordinator now has a **much simpler** OAuth2 implementation that actually works! Here's what I've done:
|
||||
|
||||
### 🔧 Simplified Implementation
|
||||
- **Removed complex Passport.js** - No more confusing middleware chains
|
||||
- **Simple JWT tokens** - Clean, stateless authentication
|
||||
- **Direct Google API calls** - Using fetch instead of heavy libraries
|
||||
- **Clean error handling** - Easy to debug and understand
|
||||
|
||||
### 📁 New Files Created
|
||||
- `backend/src/config/simpleAuth.ts` - Core auth functions
|
||||
- `backend/src/routes/simpleAuth.ts` - Auth endpoints
|
||||
|
||||
## 🚀 How to Set Up Google OAuth2
|
||||
|
||||
### Step 1: Get Google OAuth2 Credentials
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select existing one
|
||||
3. Enable the Google+ API
|
||||
4. Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client IDs"
|
||||
5. Set application type to "Web application"
|
||||
6. Add these redirect URIs:
|
||||
- `http://localhost:3000/auth/google/callback`
|
||||
- `http://localhost:5173/auth/callback`
|
||||
|
||||
### Step 2: Update Environment Variables
|
||||
|
||||
Edit `backend/.env` and add:
|
||||
|
||||
```bash
|
||||
# Google OAuth2 Settings
|
||||
GOOGLE_CLIENT_ID=your_google_client_id_here
|
||||
GOOGLE_CLIENT_SECRET=your_google_client_secret_here
|
||||
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
|
||||
|
||||
# JWT Secret (change this!)
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
```
|
||||
|
||||
### Step 3: Test the Setup
|
||||
|
||||
1. **Start the application:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
2. **Test auth endpoints:**
|
||||
```bash
|
||||
# Check if backend is running
|
||||
curl http://localhost:3000/api/health
|
||||
|
||||
# Check auth status (should return {"authenticated":false})
|
||||
curl http://localhost:3000/auth/status
|
||||
```
|
||||
|
||||
3. **Test Google OAuth flow:**
|
||||
- Visit: `http://localhost:3000/auth/google`
|
||||
- Should redirect to Google login
|
||||
- After login, redirects back with JWT token
|
||||
|
||||
## 🔄 How It Works
|
||||
|
||||
### Simple Flow:
|
||||
1. User clicks "Login with Google"
|
||||
2. Redirects to `http://localhost:3000/auth/google`
|
||||
3. Backend redirects to Google OAuth
|
||||
4. Google redirects back to `/auth/google/callback`
|
||||
5. Backend exchanges code for user info
|
||||
6. Backend creates JWT token
|
||||
7. Frontend receives token and stores it
|
||||
|
||||
### API Endpoints:
|
||||
- `GET /auth/google` - Start OAuth flow
|
||||
- `GET /auth/google/callback` - Handle OAuth callback
|
||||
- `GET /auth/status` - Check if user is authenticated
|
||||
- `GET /auth/me` - Get current user info (requires auth)
|
||||
- `POST /auth/logout` - Logout (client-side token removal)
|
||||
|
||||
## 🛠️ Frontend Integration
|
||||
|
||||
The frontend needs to:
|
||||
|
||||
1. **Handle the OAuth callback:**
|
||||
```javascript
|
||||
// In your React app, handle the callback route
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get('token');
|
||||
if (token) {
|
||||
localStorage.setItem('authToken', token);
|
||||
// Redirect to dashboard
|
||||
}
|
||||
```
|
||||
|
||||
2. **Include token in API requests:**
|
||||
```javascript
|
||||
const token = localStorage.getItem('authToken');
|
||||
fetch('/api/vips', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
3. **Add login button:**
|
||||
```javascript
|
||||
<button onClick={() => window.location.href = '/auth/google'}>
|
||||
Login with Google
|
||||
</button>
|
||||
```
|
||||
|
||||
## 🎯 Benefits of This Approach
|
||||
|
||||
- **Simple to understand** - No complex middleware
|
||||
- **Easy to debug** - Clear error messages
|
||||
- **Lightweight** - Fewer dependencies
|
||||
- **Secure** - Uses standard JWT tokens
|
||||
- **Flexible** - Easy to extend or modify
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Common Issues:
|
||||
|
||||
1. **"OAuth not configured" error:**
|
||||
- Make sure `GOOGLE_CLIENT_ID` is set in `.env`
|
||||
- Restart the backend after changing `.env`
|
||||
|
||||
2. **"Invalid redirect URI" error:**
|
||||
- Check Google Console redirect URIs match exactly
|
||||
- Make sure no trailing slashes
|
||||
|
||||
3. **Token verification fails:**
|
||||
- Check `JWT_SECRET` is set and consistent
|
||||
- Make sure token is being sent with `Bearer ` prefix
|
||||
|
||||
### Debug Commands:
|
||||
```bash
|
||||
# Check backend logs
|
||||
docker-compose -f docker-compose.dev.yml logs backend
|
||||
|
||||
# Check if environment variables are loaded
|
||||
docker exec vip-coordinator-backend-1 env | grep GOOGLE
|
||||
```
|
||||
|
||||
## 🎉 Next Steps
|
||||
|
||||
1. Set up your Google OAuth2 credentials
|
||||
2. Update the `.env` file
|
||||
3. Test the login flow
|
||||
4. Integrate with the frontend
|
||||
5. Customize user roles and permissions
|
||||
|
||||
The authentication system is now much simpler and actually works! 🚀
|
||||
@@ -1,125 +0,0 @@
|
||||
# 🔐 Simple User Management System
|
||||
|
||||
## ✅ What We Built
|
||||
|
||||
A **lightweight, persistent user management system** that extends your existing OAuth2 authentication using your existing JSON data storage.
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
### ✅ **Persistent Storage**
|
||||
- Uses your existing JSON data file storage
|
||||
- No third-party services required
|
||||
- Completely self-contained
|
||||
- Users preserved across server restarts
|
||||
|
||||
### 🔧 **New API Endpoints**
|
||||
- `GET /auth/users` - List all users (admin only)
|
||||
- `PATCH /auth/users/:email/role` - Update user role (admin only)
|
||||
- `DELETE /auth/users/:email` - Delete user (admin only)
|
||||
- `GET /auth/users/:email` - Get specific user (admin only)
|
||||
|
||||
### 🎨 **Admin Interface**
|
||||
- Beautiful React component for user management
|
||||
- Role-based access control (admin only)
|
||||
- Change user roles with dropdown
|
||||
- Delete users with confirmation
|
||||
- Responsive design
|
||||
|
||||
## 🚀 How It Works
|
||||
|
||||
### 1. **User Registration**
|
||||
- First user becomes administrator automatically
|
||||
- Subsequent users become coordinators by default
|
||||
- All via your existing Google OAuth flow
|
||||
|
||||
### 2. **Role Management**
|
||||
- **Administrator:** Full access including user management
|
||||
- **Coordinator:** Can manage VIPs, drivers, schedules
|
||||
- **Driver:** Can view assigned schedules
|
||||
|
||||
### 3. **User Management Interface**
|
||||
- Only administrators can access user management
|
||||
- View all users with profile pictures
|
||||
- Change roles instantly
|
||||
- Delete users (except yourself)
|
||||
- Clear role descriptions
|
||||
|
||||
## 📋 Usage
|
||||
|
||||
### For Administrators:
|
||||
1. Login with Google (first user becomes admin)
|
||||
2. Access user management interface
|
||||
3. View all registered users
|
||||
4. Change user roles as needed
|
||||
5. Remove users if necessary
|
||||
|
||||
### API Examples:
|
||||
```bash
|
||||
# List all users (admin only)
|
||||
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
http://localhost:3000/auth/users
|
||||
|
||||
# Update user role
|
||||
curl -X PATCH \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"role": "administrator"}' \
|
||||
http://localhost:3000/auth/users/user@example.com/role
|
||||
|
||||
# Delete user
|
||||
curl -X DELETE \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||
http://localhost:3000/auth/users/user@example.com
|
||||
```
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
- **Role-based access control** - Only admins can manage users
|
||||
- **Self-deletion prevention** - Admins can't delete themselves
|
||||
- **JWT token validation** - All endpoints require authentication
|
||||
- **Input validation** - Role validation on updates
|
||||
|
||||
## ✅ Important Notes
|
||||
|
||||
### **Persistent File Storage**
|
||||
- Users are stored in your existing JSON data file
|
||||
- **Users are preserved across server restarts**
|
||||
- Perfect for development and production
|
||||
- Integrates seamlessly with your existing data storage
|
||||
|
||||
### **Simple & Lightweight**
|
||||
- No external dependencies
|
||||
- No complex setup required
|
||||
- Works with your existing OAuth system
|
||||
- Easy to understand and modify
|
||||
|
||||
## 🎯 Perfect For
|
||||
|
||||
- **Development and production environments**
|
||||
- **Small to medium teams** (< 100 users)
|
||||
- **Self-hosted applications**
|
||||
- **When you want full control** over your user data
|
||||
- **Simple, reliable user management**
|
||||
|
||||
## 🔄 Future Enhancements
|
||||
|
||||
You can easily extend this to:
|
||||
- Migrate to your existing PostgreSQL database if needed
|
||||
- Add user metadata and profiles
|
||||
- Implement audit logging
|
||||
- Add email notifications
|
||||
- Create user groups/teams
|
||||
- Add Redis caching for better performance
|
||||
|
||||
## 🎉 Ready to Use!
|
||||
|
||||
Your user management system is now complete and ready to use:
|
||||
|
||||
1. **Restart your backend** to pick up the new endpoints
|
||||
2. **Login as the first user** to become administrator
|
||||
3. **Access user management** through your admin interface
|
||||
4. **Manage users** with the beautiful interface we built
|
||||
|
||||
**✅ Persistent storage:** All user data is automatically saved to your existing JSON data file and preserved across server restarts!
|
||||
|
||||
No external dependencies, no complex setup - just simple, effective, persistent user management! 🚀
|
||||
@@ -1,197 +0,0 @@
|
||||
# 🔐 User Management System Recommendations
|
||||
|
||||
## Current State Analysis
|
||||
✅ **You have:** Basic OAuth2 with Google, JWT tokens, role-based access (administrator/coordinator)
|
||||
❌ **You need:** Comprehensive user management, permissions, user lifecycle, admin interface
|
||||
|
||||
## 🏆 Top Recommendations
|
||||
|
||||
### 1. **Supabase Auth** (Recommended - Easy Integration)
|
||||
**Why it's perfect for you:**
|
||||
- Drop-in replacement for your current auth system
|
||||
- Built-in user management dashboard
|
||||
- Row Level Security (RLS) for fine-grained permissions
|
||||
- Supports Google OAuth (you can keep your current flow)
|
||||
- Real-time subscriptions
|
||||
- Built-in user roles and metadata
|
||||
|
||||
**Integration effort:** Low (2-3 days)
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
**Features you get:**
|
||||
- User registration/login/logout
|
||||
- Email verification
|
||||
- Password reset
|
||||
- User metadata and custom claims
|
||||
- Admin dashboard for user management
|
||||
- Real-time user presence
|
||||
- Multi-factor authentication
|
||||
|
||||
### 2. **Auth0** (Enterprise-grade)
|
||||
**Why it's great:**
|
||||
- Industry standard for enterprise applications
|
||||
- Extensive user management dashboard
|
||||
- Advanced security features
|
||||
- Supports all OAuth providers
|
||||
- Fine-grained permissions and roles
|
||||
- Audit logs and analytics
|
||||
|
||||
**Integration effort:** Medium (3-5 days)
|
||||
```bash
|
||||
npm install auth0 express-oauth-server
|
||||
```
|
||||
|
||||
**Features you get:**
|
||||
- Complete user lifecycle management
|
||||
- Advanced role-based access control (RBAC)
|
||||
- Multi-factor authentication
|
||||
- Social logins (Google, Facebook, etc.)
|
||||
- Enterprise SSO
|
||||
- Comprehensive admin dashboard
|
||||
|
||||
### 3. **Firebase Auth + Firestore** (Google Ecosystem)
|
||||
**Why it fits:**
|
||||
- You're already using Google OAuth
|
||||
- Seamless integration with Google services
|
||||
- Real-time database
|
||||
- Built-in user management
|
||||
- Offline support
|
||||
|
||||
**Integration effort:** Medium (4-6 days)
|
||||
```bash
|
||||
npm install firebase-admin
|
||||
```
|
||||
|
||||
### 4. **Clerk** (Modern Developer Experience)
|
||||
**Why developers love it:**
|
||||
- Beautiful pre-built UI components
|
||||
- Excellent TypeScript support
|
||||
- Built-in user management dashboard
|
||||
- Easy role and permission management
|
||||
- Great documentation
|
||||
|
||||
**Integration effort:** Low-Medium (2-4 days)
|
||||
```bash
|
||||
npm install @clerk/clerk-sdk-node
|
||||
```
|
||||
|
||||
## 🎯 My Recommendation: **Supabase Auth**
|
||||
|
||||
### Why Supabase is perfect for your project:
|
||||
|
||||
1. **Minimal code changes** - Can integrate with your existing JWT system
|
||||
2. **Built-in admin dashboard** - No need to build user management UI
|
||||
3. **PostgreSQL-based** - Familiar database, easy to extend
|
||||
4. **Real-time features** - Perfect for your VIP coordination needs
|
||||
5. **Row Level Security** - Fine-grained permissions per user/role
|
||||
6. **Free tier** - Great for development and small deployments
|
||||
|
||||
### Quick Integration Plan:
|
||||
|
||||
#### Step 1: Setup Supabase Project
|
||||
```bash
|
||||
# Install Supabase
|
||||
npm install @supabase/supabase-js
|
||||
|
||||
# Create project at https://supabase.com
|
||||
# Get your project URL and anon key
|
||||
```
|
||||
|
||||
#### Step 2: Replace your user storage
|
||||
```typescript
|
||||
// Instead of: const users: Map<string, User> = new Map();
|
||||
// Use Supabase's built-in auth.users table
|
||||
```
|
||||
|
||||
#### Step 3: Add user management endpoints
|
||||
```typescript
|
||||
// Get all users (admin only)
|
||||
router.get('/users', requireAuth, requireRole(['administrator']), async (req, res) => {
|
||||
const { data: users } = await supabase.auth.admin.listUsers();
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
// Update user role
|
||||
router.patch('/users/:id/role', requireAuth, requireRole(['administrator']), async (req, res) => {
|
||||
const { role } = req.body;
|
||||
const { data } = await supabase.auth.admin.updateUserById(req.params.id, {
|
||||
user_metadata: { role }
|
||||
});
|
||||
res.json(data);
|
||||
});
|
||||
```
|
||||
|
||||
#### Step 4: Add frontend user management
|
||||
- Use Supabase's built-in dashboard OR
|
||||
- Build simple admin interface with user list/edit/delete
|
||||
|
||||
## 🚀 Implementation Options
|
||||
|
||||
### Option A: Quick Integration (Keep your current system + add Supabase)
|
||||
- Keep your current OAuth flow
|
||||
- Add Supabase for user storage and management
|
||||
- Use Supabase dashboard for admin tasks
|
||||
- **Time:** 2-3 days
|
||||
|
||||
### Option B: Full Migration (Replace with Supabase Auth)
|
||||
- Migrate to Supabase Auth completely
|
||||
- Use their OAuth providers
|
||||
- Get all advanced features
|
||||
- **Time:** 4-5 days
|
||||
|
||||
### Option C: Custom Admin Interface
|
||||
- Keep your current system
|
||||
- Build custom React admin interface
|
||||
- Add user CRUD operations
|
||||
- **Time:** 1-2 weeks
|
||||
|
||||
## 📋 Next Steps
|
||||
|
||||
1. **Choose your approach** (I recommend Option A - Quick Integration)
|
||||
2. **Set up Supabase project** (5 minutes)
|
||||
3. **Integrate user storage** (1 day)
|
||||
4. **Add admin endpoints** (1 day)
|
||||
5. **Test and refine** (1 day)
|
||||
|
||||
## 🔧 Alternative: Lightweight Custom Solution
|
||||
|
||||
If you prefer to keep it simple and custom:
|
||||
|
||||
```typescript
|
||||
// Add these endpoints to your existing auth system:
|
||||
|
||||
// List all users (admin only)
|
||||
router.get('/users', requireAuth, requireRole(['administrator']), (req, res) => {
|
||||
const userList = Array.from(users.values()).map(user => ({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
lastLogin: user.lastLogin
|
||||
}));
|
||||
res.json(userList);
|
||||
});
|
||||
|
||||
// Update user role
|
||||
router.patch('/users/:email/role', requireAuth, requireRole(['administrator']), (req, res) => {
|
||||
const { role } = req.body;
|
||||
const user = users.get(req.params.email);
|
||||
if (user) {
|
||||
user.role = role;
|
||||
users.set(req.params.email, user);
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete user
|
||||
router.delete('/users/:email', requireAuth, requireRole(['administrator']), (req, res) => {
|
||||
users.delete(req.params.email);
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
Would you like me to help you implement any of these options?
|
||||
@@ -1,140 +0,0 @@
|
||||
# 🌐 Web Server Proxy Configuration for OAuth
|
||||
|
||||
## 🎯 Problem Identified
|
||||
|
||||
Your domain `bsa.madeamess.online` is not properly configured to proxy requests to your Docker containers. When Google redirects to `https://bsa.madeamess.online:5173/auth/google/callback`, it gets "ERR_CONNECTION_REFUSED" because there's no web server listening on port 5173 for your domain.
|
||||
|
||||
## 🔧 Solution Options
|
||||
|
||||
### Option 1: Configure Nginx Proxy (Recommended)
|
||||
|
||||
If you're using nginx, add this configuration:
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/bsa.madeamess.online
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name bsa.madeamess.online;
|
||||
|
||||
# SSL configuration (your existing SSL setup)
|
||||
ssl_certificate /path/to/your/certificate.crt;
|
||||
ssl_certificate_key /path/to/your/private.key;
|
||||
|
||||
# Proxy to your Docker frontend container
|
||||
location / {
|
||||
proxy_pass http://localhost:5173;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Important: Handle all routes for SPA
|
||||
try_files $uri $uri/ @fallback;
|
||||
}
|
||||
|
||||
# Fallback for SPA routing
|
||||
location @fallback {
|
||||
proxy_pass http://localhost:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name bsa.madeamess.online;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Configure Apache Proxy
|
||||
|
||||
If you're using Apache, add this to your virtual host:
|
||||
|
||||
```apache
|
||||
<VirtualHost *:443>
|
||||
ServerName bsa.madeamess.online
|
||||
|
||||
# SSL configuration (your existing SSL setup)
|
||||
SSLEngine on
|
||||
SSLCertificateFile /path/to/your/certificate.crt
|
||||
SSLCertificateKeyFile /path/to/your/private.key
|
||||
|
||||
# Enable proxy modules
|
||||
ProxyPreserveHost On
|
||||
ProxyRequests Off
|
||||
|
||||
# Proxy to your Docker frontend container
|
||||
ProxyPass / http://localhost:5173/
|
||||
ProxyPassReverse / http://localhost:5173/
|
||||
|
||||
# Handle WebSocket connections for Vite HMR
|
||||
ProxyPass /ws ws://localhost:5173/ws
|
||||
ProxyPassReverse /ws ws://localhost:5173/ws
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName bsa.madeamess.online
|
||||
Redirect permanent / https://bsa.madeamess.online/
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### Option 3: Update Google OAuth Redirect URI (Quick Fix)
|
||||
|
||||
**Temporary workaround:** Update your Google Cloud Console OAuth settings to use `http://localhost:5173/auth/google/callback` instead of your domain, then access your app directly via `http://localhost:5173`.
|
||||
|
||||
## 🔄 Alternative: Use Standard Ports
|
||||
|
||||
### Option 4: Configure to use standard ports (80/443)
|
||||
|
||||
Modify your docker-compose to use standard ports:
|
||||
|
||||
```yaml
|
||||
# In docker-compose.dev.yml
|
||||
services:
|
||||
frontend:
|
||||
ports:
|
||||
- "80:5173" # HTTP
|
||||
# or
|
||||
- "443:5173" # HTTPS (requires SSL setup in container)
|
||||
```
|
||||
|
||||
Then update Google OAuth redirect URI to:
|
||||
- `https://bsa.madeamess.online/auth/google/callback` (no port)
|
||||
|
||||
## 🧪 Testing Steps
|
||||
|
||||
1. **Apply web server configuration**
|
||||
2. **Restart your web server:**
|
||||
```bash
|
||||
# For nginx
|
||||
sudo systemctl reload nginx
|
||||
|
||||
# For Apache
|
||||
sudo systemctl reload apache2
|
||||
```
|
||||
3. **Test the proxy:**
|
||||
```bash
|
||||
curl -I https://bsa.madeamess.online
|
||||
```
|
||||
4. **Test OAuth flow:**
|
||||
- Visit `https://bsa.madeamess.online`
|
||||
- Click "Continue with Google"
|
||||
- Complete authentication
|
||||
- Should redirect back successfully
|
||||
|
||||
## 🎯 Root Cause Summary
|
||||
|
||||
The OAuth callback was failing because:
|
||||
1. ✅ **Frontend routing** - Fixed (React Router now handles callback)
|
||||
2. ✅ **CORS configuration** - Fixed (Backend accepts your domain)
|
||||
3. ❌ **Web server proxy** - **NEEDS FIXING** (Domain not proxying to Docker)
|
||||
|
||||
Once you configure your web server to proxy `bsa.madeamess.online` to `localhost:5173`, the OAuth flow will work perfectly!
|
||||
148
api-docs.html
148
api-docs.html
@@ -1,148 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VIP Coordinator API Documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
body {
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
.swagger-ui .topbar {
|
||||
background-color: #3498db;
|
||||
}
|
||||
.swagger-ui .topbar .download-url-wrapper .select-label {
|
||||
color: white;
|
||||
}
|
||||
.swagger-ui .topbar .download-url-wrapper input[type=text] {
|
||||
border: 2px solid #2980b9;
|
||||
}
|
||||
.swagger-ui .info .title {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.custom-header {
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.custom-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
font-weight: 300;
|
||||
}
|
||||
.custom-header p {
|
||||
margin: 10px 0 0 0;
|
||||
font-size: 1.2em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.quick-links {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.quick-links h3 {
|
||||
color: #2c3e50;
|
||||
margin-top: 0;
|
||||
}
|
||||
.quick-links ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.quick-links li {
|
||||
background: #ecf0f1;
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
.quick-links li strong {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.quick-links li code {
|
||||
background: #34495e;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="custom-header">
|
||||
<h1>🚗 VIP Coordinator API</h1>
|
||||
<p>Comprehensive API for managing VIP transportation coordination</p>
|
||||
</div>
|
||||
|
||||
<div class="quick-links">
|
||||
<h3>🚀 Quick Start Examples</h3>
|
||||
<ul>
|
||||
<li><strong>Health Check:</strong> <code>GET /api/health</code></li>
|
||||
<li><strong>Get All VIPs:</strong> <code>GET /api/vips</code></li>
|
||||
<li><strong>Get All Drivers:</strong> <code>GET /api/drivers</code></li>
|
||||
<li><strong>Flight Info:</strong> <code>GET /api/flights/UA1234?date=2025-06-26</code></li>
|
||||
<li><strong>VIP Schedule:</strong> <code>GET /api/vips/{vipId}/schedule</code></li>
|
||||
<li><strong>Driver Availability:</strong> <code>POST /api/drivers/availability</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: './api-documentation.yaml',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout",
|
||||
tryItOutEnabled: true,
|
||||
requestInterceptor: function(request) {
|
||||
// Add base URL if not present
|
||||
if (request.url.startsWith('/api/')) {
|
||||
request.url = 'http://localhost:3000' + request.url;
|
||||
}
|
||||
return request;
|
||||
},
|
||||
onComplete: function() {
|
||||
console.log('VIP Coordinator API Documentation loaded successfully!');
|
||||
},
|
||||
docExpansion: 'list',
|
||||
defaultModelsExpandDepth: 2,
|
||||
defaultModelExpandDepth: 2,
|
||||
showExtensions: true,
|
||||
showCommonExtensions: true,
|
||||
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
|
||||
validatorUrl: null
|
||||
});
|
||||
// End Swagger UI call region
|
||||
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
67
backend/.dockerignore
Normal file
67
backend/.dockerignore
Normal file
@@ -0,0 +1,67 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build output
|
||||
dist
|
||||
build
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment files (will be injected at runtime)
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
*.spec.ts
|
||||
test
|
||||
tests
|
||||
**/__tests__
|
||||
**/__mocks__
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!README.md
|
||||
docs
|
||||
|
||||
# IDE and editor files
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Temporary files
|
||||
tmp
|
||||
temp
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Docker files (avoid recursion)
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
docker-compose*.yml
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
.gitlab-ci.yml
|
||||
.travis.yml
|
||||
|
||||
# Misc
|
||||
.editorconfig
|
||||
.eslintrc*
|
||||
.prettierrc*
|
||||
jest.config.js
|
||||
57
backend/.env
57
backend/.env
@@ -1,26 +1,33 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL=postgresql://postgres:changeme@db:5432/vip_coordinator
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# Authentication Configuration
|
||||
JWT_SECRET=your-super-secure-jwt-secret-key-change-in-production-12345
|
||||
SESSION_SECRET=your-super-secure-session-secret-change-in-production-67890
|
||||
|
||||
# Google OAuth Configuration (optional for local development)
|
||||
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=https://bsa.madeamess.online
|
||||
|
||||
# Flight API Configuration
|
||||
AVIATIONSTACK_API_KEY=your-aviationstack-api-key
|
||||
|
||||
# Admin Configuration
|
||||
ADMIN_PASSWORD=admin123
|
||||
|
||||
# Port Configuration
|
||||
# ============================================
|
||||
# Application Configuration
|
||||
# ============================================
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
|
||||
# ============================================
|
||||
# Database Configuration
|
||||
# ============================================
|
||||
DATABASE_URL="postgresql://postgres:changeme@localhost:5433/vip_coordinator"
|
||||
|
||||
# ============================================
|
||||
# Redis Configuration (Optional)
|
||||
# ============================================
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
|
||||
# ============================================
|
||||
# Auth0 Configuration
|
||||
# ============================================
|
||||
# Get these from your Auth0 dashboard:
|
||||
# 1. Create Application (Single Page Application)
|
||||
# 2. Create API
|
||||
# 3. Configure callback URLs: http://localhost:5173/callback
|
||||
AUTH0_DOMAIN="dev-s855cy3bvjjbkljt.us.auth0.com"
|
||||
AUTH0_AUDIENCE="https://vip-coordinator-api"
|
||||
AUTH0_ISSUER="https://dev-s855cy3bvjjbkljt.us.auth0.com/"
|
||||
|
||||
# ============================================
|
||||
# Flight Tracking API (Optional)
|
||||
# ============================================
|
||||
# Get API key from: https://aviationstack.com/
|
||||
AVIATIONSTACK_API_KEY="your-aviationstack-api-key"
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL=postgresql://postgres:password@db:5432/vip_coordinator
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# Authentication Configuration
|
||||
JWT_SECRET=your-super-secure-jwt-secret-key-change-in-production
|
||||
SESSION_SECRET=your-super-secure-session-secret-change-in-production
|
||||
|
||||
# Google OAuth Configuration
|
||||
GOOGLE_CLIENT_ID=your-google-client-id-from-console
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret-from-console
|
||||
|
||||
# Frontend URL
|
||||
# ============================================
|
||||
# Application Configuration
|
||||
# ============================================
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
|
||||
# Flight API Configuration
|
||||
AVIATIONSTACK_API_KEY=your-aviationstack-api-key
|
||||
# ============================================
|
||||
# Database Configuration
|
||||
# ============================================
|
||||
# Port 5433 is used to avoid conflicts with local PostgreSQL
|
||||
DATABASE_URL="postgresql://postgres:changeme@localhost:5433/vip_coordinator"
|
||||
|
||||
# Admin Configuration
|
||||
ADMIN_PASSWORD=admin123
|
||||
# ============================================
|
||||
# Redis Configuration (Optional)
|
||||
# ============================================
|
||||
# Port 6380 is used to avoid conflicts with local Redis
|
||||
REDIS_URL="redis://localhost:6380"
|
||||
|
||||
# ============================================
|
||||
# Auth0 Configuration
|
||||
# ============================================
|
||||
# Get these from your Auth0 dashboard:
|
||||
# 1. Create Application (Single Page Application)
|
||||
# 2. Create API
|
||||
# 3. Configure callback URLs: http://localhost:5173/callback
|
||||
AUTH0_DOMAIN="your-tenant.us.auth0.com"
|
||||
AUTH0_AUDIENCE="https://your-api-identifier"
|
||||
AUTH0_ISSUER="https://your-tenant.us.auth0.com/"
|
||||
|
||||
# ============================================
|
||||
# Flight Tracking API (Optional)
|
||||
# ============================================
|
||||
AVIATIONSTACK_API_KEY="your-aviationstack-api-key"
|
||||
|
||||
43
backend/.gitignore
vendored
Normal file
43
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# Prisma
|
||||
prisma/migrations/.migrate_lock
|
||||
@@ -1,21 +1,87 @@
|
||||
# Multi-stage build for development and production
|
||||
FROM node:22-alpine AS base
|
||||
# ==========================================
|
||||
# Stage 1: Dependencies
|
||||
# Install all dependencies and generate Prisma client
|
||||
# ==========================================
|
||||
FROM node:20-alpine AS dependencies
|
||||
|
||||
# Install OpenSSL for Prisma support
|
||||
RUN apk add --no-cache openssl libc6-compat
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Development stage
|
||||
FROM base AS development
|
||||
RUN npm install
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "dev"]
|
||||
# Install all dependencies (including dev dependencies for build)
|
||||
RUN npm ci
|
||||
|
||||
# Production stage
|
||||
FROM base AS production
|
||||
RUN npm install
|
||||
# Copy Prisma schema and generate client
|
||||
COPY prisma ./prisma
|
||||
RUN npx prisma generate
|
||||
|
||||
# ==========================================
|
||||
# Stage 2: Builder
|
||||
# Compile TypeScript application
|
||||
# ==========================================
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy node_modules from dependencies stage
|
||||
COPY --from=dependencies /app/node_modules ./node_modules
|
||||
|
||||
# Copy application source
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Install only production dependencies
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
# ==========================================
|
||||
# Stage 3: Production Runtime
|
||||
# Minimal runtime image with only necessary files
|
||||
# ==========================================
|
||||
FROM node:20-alpine AS production
|
||||
|
||||
# Install OpenSSL, dumb-init, and netcat for database health checks
|
||||
RUN apk add --no-cache openssl dumb-init netcat-openbsd
|
||||
|
||||
# Create non-root user for security
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nestjs -u 1001
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy production dependencies from builder
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist
|
||||
|
||||
# Copy Prisma schema and migrations (needed for runtime)
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/prisma ./prisma
|
||||
|
||||
# Copy package.json for metadata
|
||||
COPY --from=builder --chown=nestjs:nodejs /app/package*.json ./
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY --chown=nestjs:nodejs docker-entrypoint.sh ./
|
||||
RUN chmod +x docker-entrypoint.sh
|
||||
|
||||
# Switch to non-root user
|
||||
USER nestjs
|
||||
|
||||
# Expose application port
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3000/api/v1/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||
|
||||
# Use dumb-init to handle signals properly
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||
|
||||
# Run entrypoint script (handles migrations then starts app)
|
||||
CMD ["./docker-entrypoint.sh"]
|
||||
|
||||
134
backend/README.md
Normal file
134
backend/README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# VIP Coordinator Backend
|
||||
|
||||
NestJS 10.x backend with Prisma ORM, Auth0 authentication, and PostgreSQL.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Set up environment variables
|
||||
cp .env.example .env
|
||||
# Edit .env with your Auth0 credentials
|
||||
|
||||
# Start PostgreSQL (via Docker)
|
||||
cd ..
|
||||
docker-compose up -d postgres
|
||||
|
||||
# Generate Prisma Client
|
||||
npx prisma generate
|
||||
|
||||
# Run database migrations
|
||||
npx prisma migrate dev
|
||||
|
||||
# Seed sample data (optional)
|
||||
npm run prisma:seed
|
||||
|
||||
# Start development server
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All endpoints are prefixed with `/api/v1`
|
||||
|
||||
### Public Endpoints
|
||||
- `GET /health` - Health check
|
||||
|
||||
### Authentication
|
||||
- `GET /auth/profile` - Get current user profile
|
||||
|
||||
### Users (Admin only)
|
||||
- `GET /users` - List all users
|
||||
- `GET /users/pending` - List pending approval users
|
||||
- `GET /users/:id` - Get user by ID
|
||||
- `PATCH /users/:id` - Update user
|
||||
- `PATCH /users/:id/approve` - Approve/deny user
|
||||
- `DELETE /users/:id` - Delete user (soft)
|
||||
|
||||
### VIPs (Admin, Coordinator)
|
||||
- `GET /vips` - List all VIPs
|
||||
- `POST /vips` - Create VIP
|
||||
- `GET /vips/:id` - Get VIP by ID
|
||||
- `PATCH /vips/:id` - Update VIP
|
||||
- `DELETE /vips/:id` - Delete VIP (soft)
|
||||
|
||||
### Drivers (Admin, Coordinator)
|
||||
- `GET /drivers` - List all drivers
|
||||
- `POST /drivers` - Create driver
|
||||
- `GET /drivers/:id` - Get driver by ID
|
||||
- `GET /drivers/:id/schedule` - Get driver schedule
|
||||
- `PATCH /drivers/:id` - Update driver
|
||||
- `DELETE /drivers/:id` - Delete driver (soft)
|
||||
|
||||
### Events (Admin, Coordinator; Drivers can view and update status)
|
||||
- `GET /events` - List all events
|
||||
- `POST /events` - Create event (with conflict detection)
|
||||
- `GET /events/:id` - Get event by ID
|
||||
- `PATCH /events/:id` - Update event
|
||||
- `PATCH /events/:id/status` - Update event status
|
||||
- `DELETE /events/:id` - Delete event (soft)
|
||||
|
||||
### Flights (Admin, Coordinator)
|
||||
- `GET /flights` - List all flights
|
||||
- `POST /flights` - Create flight
|
||||
- `GET /flights/status/:flightNumber` - Get real-time flight status
|
||||
- `GET /flights/vip/:vipId` - Get flights for VIP
|
||||
- `GET /flights/:id` - Get flight by ID
|
||||
- `PATCH /flights/:id` - Update flight
|
||||
- `DELETE /flights/:id` - Delete flight
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
npm run start:dev # Start dev server with hot reload
|
||||
npm run build # Build for production
|
||||
npm run start:prod # Start production server
|
||||
npm run lint # Run ESLint
|
||||
npm run test # Run tests
|
||||
npm run test:watch # Run tests in watch mode
|
||||
npm run test:cov # Run tests with coverage
|
||||
```
|
||||
|
||||
## Database Commands
|
||||
|
||||
```bash
|
||||
npx prisma studio # Open Prisma Studio (database GUI)
|
||||
npx prisma migrate dev # Create and apply migration
|
||||
npx prisma migrate deploy # Apply migrations (production)
|
||||
npx prisma migrate reset # Reset database (DEV ONLY)
|
||||
npx prisma generate # Regenerate Prisma Client
|
||||
npm run prisma:seed # Seed database with sample data
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
See `.env.example` for all required variables:
|
||||
|
||||
- `DATABASE_URL` - PostgreSQL connection string
|
||||
- `AUTH0_DOMAIN` - Your Auth0 tenant domain
|
||||
- `AUTH0_AUDIENCE` - Your Auth0 API identifier
|
||||
- `AUTH0_ISSUER` - Your Auth0 issuer URL
|
||||
- `AVIATIONSTACK_API_KEY` - Flight tracking API key (optional)
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ Auth0 JWT authentication
|
||||
- ✅ Role-based access control (Administrator, Coordinator, Driver)
|
||||
- ✅ User approval workflow
|
||||
- ✅ VIP management
|
||||
- ✅ Driver management
|
||||
- ✅ Event scheduling with conflict detection
|
||||
- ✅ Flight tracking integration
|
||||
- ✅ Soft deletes for all entities
|
||||
- ✅ Comprehensive validation
|
||||
- ✅ Type-safe database queries with Prisma
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** NestJS 10.x
|
||||
- **Database:** PostgreSQL 15+ with Prisma 5.x ORM
|
||||
- **Authentication:** Auth0 + Passport JWT
|
||||
- **Validation:** class-validator + class-transformer
|
||||
- **HTTP Client:** @nestjs/axios (for flight tracking)
|
||||
85
backend/docker-entrypoint.sh
Normal file
85
backend/docker-entrypoint.sh
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo "=== VIP Coordinator Backend - Starting ==="
|
||||
|
||||
# Function to wait for PostgreSQL to be ready
|
||||
wait_for_postgres() {
|
||||
echo "Waiting for PostgreSQL to be ready..."
|
||||
|
||||
# Extract host and port from DATABASE_URL
|
||||
# Format: postgresql://user:pass@host:port/dbname
|
||||
DB_HOST=$(echo $DATABASE_URL | sed -n 's/.*@\(.*\):.*/\1/p')
|
||||
DB_PORT=$(echo $DATABASE_URL | sed -n 's/.*:\([0-9]*\)\/.*/\1/p')
|
||||
|
||||
# Default to standard PostgreSQL port if not found
|
||||
DB_PORT=${DB_PORT:-5432}
|
||||
|
||||
echo "Checking PostgreSQL at ${DB_HOST}:${DB_PORT}..."
|
||||
|
||||
# Wait up to 60 seconds for PostgreSQL
|
||||
timeout=60
|
||||
counter=0
|
||||
|
||||
until nc -z "$DB_HOST" "$DB_PORT" 2>/dev/null; do
|
||||
counter=$((counter + 1))
|
||||
if [ $counter -gt $timeout ]; then
|
||||
echo "ERROR: PostgreSQL not available after ${timeout} seconds"
|
||||
exit 1
|
||||
fi
|
||||
echo "PostgreSQL not ready yet... waiting (${counter}/${timeout})"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "✓ PostgreSQL is ready!"
|
||||
}
|
||||
|
||||
# Function to run database migrations
|
||||
run_migrations() {
|
||||
echo "Running database migrations..."
|
||||
|
||||
if npx prisma migrate deploy; then
|
||||
echo "✓ Migrations completed successfully!"
|
||||
else
|
||||
echo "ERROR: Migration failed!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to seed database (optional)
|
||||
seed_database() {
|
||||
if [ "$RUN_SEED" = "true" ]; then
|
||||
echo "Seeding database..."
|
||||
|
||||
if npx prisma db seed; then
|
||||
echo "✓ Database seeded successfully!"
|
||||
else
|
||||
echo "WARNING: Database seeding failed (continuing anyway)"
|
||||
fi
|
||||
else
|
||||
echo "Skipping database seeding (RUN_SEED not set to 'true')"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
# Wait for database to be available
|
||||
wait_for_postgres
|
||||
|
||||
# Run migrations
|
||||
run_migrations
|
||||
|
||||
# Optionally seed database
|
||||
seed_database
|
||||
|
||||
echo "=== Starting NestJS Application ==="
|
||||
echo "Node version: $(node --version)"
|
||||
echo "Environment: ${NODE_ENV:-production}"
|
||||
echo "Starting server on port 3000..."
|
||||
|
||||
# Start the application
|
||||
exec node dist/src/main
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
8
backend/nest-cli.json
Normal file
8
backend/nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
10776
backend/package-lock.json
generated
10776
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,41 +1,93 @@
|
||||
{
|
||||
"name": "vip-coordinator-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend API for VIP Coordinator Dashboard",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"dev": "npx tsx src/index.ts",
|
||||
"build": "tsc",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"vip",
|
||||
"coordinator",
|
||||
"dashboard",
|
||||
"api"
|
||||
],
|
||||
"description": "VIP Coordinator Backend API - NestJS + Prisma + Auth0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:studio": "prisma studio",
|
||||
"prisma:seed": "ts-node prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.11.3",
|
||||
"redis": "^4.6.8",
|
||||
"uuid": "^9.0.0"
|
||||
"@casl/ability": "^6.8.0",
|
||||
"@casl/prisma": "^1.6.1",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/common": "^10.3.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@prisma/client": "^5.8.1",
|
||||
"axios": "^1.6.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/node": "^20.5.0",
|
||||
"@types/pg": "^8.10.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.6.0"
|
||||
"@nestjs/cli": "^10.2.1",
|
||||
"@nestjs/schematics": "^10.0.3",
|
||||
"@nestjs/testing": "^10.3.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/passport-jwt": "^4.0.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.8.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node prisma/seed.ts"
|
||||
}
|
||||
}
|
||||
|
||||
137
backend/prisma/migrations/20260125085806_init/migration.sql
Normal file
137
backend/prisma/migrations/20260125085806_init/migration.sql
Normal file
@@ -0,0 +1,137 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Role" AS ENUM ('ADMINISTRATOR', 'COORDINATOR', 'DRIVER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Department" AS ENUM ('OFFICE_OF_DEVELOPMENT', 'ADMIN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ArrivalMode" AS ENUM ('FLIGHT', 'SELF_DRIVING');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EventType" AS ENUM ('TRANSPORT', 'MEETING', 'EVENT', 'MEAL', 'ACCOMMODATION');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EventStatus" AS ENUM ('SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL,
|
||||
"auth0Id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"picture" TEXT,
|
||||
"role" "Role" NOT NULL DEFAULT 'COORDINATOR',
|
||||
"isApproved" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "vips" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"organization" TEXT,
|
||||
"department" "Department" NOT NULL,
|
||||
"arrivalMode" "ArrivalMode" NOT NULL,
|
||||
"expectedArrival" TIMESTAMP(3),
|
||||
"airportPickup" BOOLEAN NOT NULL DEFAULT false,
|
||||
"venueTransport" BOOLEAN NOT NULL DEFAULT false,
|
||||
"notes" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "vips_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "flights" (
|
||||
"id" TEXT NOT NULL,
|
||||
"vipId" TEXT NOT NULL,
|
||||
"flightNumber" TEXT NOT NULL,
|
||||
"flightDate" TIMESTAMP(3) NOT NULL,
|
||||
"segment" INTEGER NOT NULL DEFAULT 1,
|
||||
"departureAirport" TEXT NOT NULL,
|
||||
"arrivalAirport" TEXT NOT NULL,
|
||||
"scheduledDeparture" TIMESTAMP(3),
|
||||
"scheduledArrival" TIMESTAMP(3),
|
||||
"actualDeparture" TIMESTAMP(3),
|
||||
"actualArrival" TIMESTAMP(3),
|
||||
"status" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "flights_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "drivers" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"phone" TEXT NOT NULL,
|
||||
"department" "Department",
|
||||
"userId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "drivers_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "schedule_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"vipId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"location" TEXT,
|
||||
"startTime" TIMESTAMP(3) NOT NULL,
|
||||
"endTime" TIMESTAMP(3) NOT NULL,
|
||||
"description" TEXT,
|
||||
"type" "EventType" NOT NULL DEFAULT 'TRANSPORT',
|
||||
"status" "EventStatus" NOT NULL DEFAULT 'SCHEDULED',
|
||||
"driverId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "schedule_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_auth0Id_key" ON "users"("auth0Id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "flights_vipId_idx" ON "flights"("vipId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "flights_flightNumber_flightDate_idx" ON "flights"("flightNumber", "flightDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "drivers_userId_key" ON "drivers"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "schedule_events_vipId_idx" ON "schedule_events"("vipId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "schedule_events_driverId_idx" ON "schedule_events"("driverId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "schedule_events_startTime_endTime_idx" ON "schedule_events"("startTime", "endTime");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "flights" ADD CONSTRAINT "flights_vipId_fkey" FOREIGN KEY ("vipId") REFERENCES "vips"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "drivers" ADD CONSTRAINT "drivers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "schedule_events" ADD CONSTRAINT "schedule_events_vipId_fkey" FOREIGN KEY ("vipId") REFERENCES "vips"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "schedule_events" ADD CONSTRAINT "schedule_events_driverId_fkey" FOREIGN KEY ("driverId") REFERENCES "drivers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,50 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "VehicleType" AS ENUM ('VAN', 'SUV', 'SEDAN', 'BUS', 'GOLF_CART', 'TRUCK');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "VehicleStatus" AS ENUM ('AVAILABLE', 'IN_USE', 'MAINTENANCE', 'RESERVED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "drivers" ADD COLUMN "isAvailable" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "shiftEndTime" TIMESTAMP(3),
|
||||
ADD COLUMN "shiftStartTime" TIMESTAMP(3);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "schedule_events" ADD COLUMN "actualEndTime" TIMESTAMP(3),
|
||||
ADD COLUMN "actualStartTime" TIMESTAMP(3),
|
||||
ADD COLUMN "dropoffLocation" TEXT,
|
||||
ADD COLUMN "notes" TEXT,
|
||||
ADD COLUMN "pickupLocation" TEXT,
|
||||
ADD COLUMN "vehicleId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "vehicles" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "VehicleType" NOT NULL DEFAULT 'VAN',
|
||||
"licensePlate" TEXT,
|
||||
"seatCapacity" INTEGER NOT NULL,
|
||||
"status" "VehicleStatus" NOT NULL DEFAULT 'AVAILABLE',
|
||||
"currentDriverId" TEXT,
|
||||
"notes" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "vehicles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "vehicles_currentDriverId_key" ON "vehicles"("currentDriverId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "schedule_events_vehicleId_idx" ON "schedule_events"("vehicleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "schedule_events_status_idx" ON "schedule_events"("status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_currentDriverId_fkey" FOREIGN KEY ("currentDriverId") REFERENCES "drivers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "schedule_events" ADD CONSTRAINT "schedule_events_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,74 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "schedule_events" ADD COLUMN "eventId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "event_templates" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"defaultDuration" INTEGER NOT NULL DEFAULT 60,
|
||||
"location" TEXT,
|
||||
"type" "EventType" NOT NULL DEFAULT 'EVENT',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "event_templates_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"startTime" TIMESTAMP(3) NOT NULL,
|
||||
"endTime" TIMESTAMP(3) NOT NULL,
|
||||
"location" TEXT NOT NULL,
|
||||
"type" "EventType" NOT NULL DEFAULT 'EVENT',
|
||||
"templateId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "event_attendance" (
|
||||
"id" TEXT NOT NULL,
|
||||
"eventId" TEXT NOT NULL,
|
||||
"vipId" TEXT NOT NULL,
|
||||
"addedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "event_attendance_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_startTime_endTime_idx" ON "events"("startTime", "endTime");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "events_templateId_idx" ON "events"("templateId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "event_attendance_eventId_idx" ON "event_attendance"("eventId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "event_attendance_vipId_idx" ON "event_attendance"("vipId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "event_attendance_eventId_vipId_key" ON "event_attendance"("eventId", "vipId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "schedule_events_eventId_idx" ON "schedule_events"("eventId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "schedule_events" ADD CONSTRAINT "schedule_events_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "events"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "events" ADD CONSTRAINT "events_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "event_templates"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "event_attendance" ADD CONSTRAINT "event_attendance_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "events"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "event_attendance" ADD CONSTRAINT "event_attendance_vipId_fkey" FOREIGN KEY ("vipId") REFERENCES "vips"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `vipId` on the `schedule_events` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "schedule_events" DROP CONSTRAINT "schedule_events_vipId_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "schedule_events_vipId_idx";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "schedule_events" DROP COLUMN "vipId",
|
||||
ADD COLUMN "vipIds" TEXT[];
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Drop the event_attendance join table first (has foreign keys)
|
||||
DROP TABLE IF EXISTS "event_attendance" CASCADE;
|
||||
|
||||
-- Drop the events table (references event_templates)
|
||||
DROP TABLE IF EXISTS "events" CASCADE;
|
||||
|
||||
-- Drop the event_templates table
|
||||
DROP TABLE IF EXISTS "event_templates" CASCADE;
|
||||
|
||||
-- Drop the eventId column from schedule_events (referenced dropped events table)
|
||||
ALTER TABLE "schedule_events" DROP COLUMN IF EXISTS "eventId";
|
||||
3
backend/prisma/migrations/migration_lock.toml
Normal file
3
backend/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
226
backend/prisma/schema.prisma
Normal file
226
backend/prisma/schema.prisma
Normal file
@@ -0,0 +1,226 @@
|
||||
// VIP Coordinator - Prisma Schema
|
||||
// This is your database schema (source of truth)
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// User Management
|
||||
// ============================================
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
auth0Id String @unique // Auth0 sub claim
|
||||
email String @unique
|
||||
name String?
|
||||
picture String?
|
||||
role Role @default(COORDINATOR)
|
||||
isApproved Boolean @default(false)
|
||||
driver Driver? // Optional linked driver account
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime? // Soft delete
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
enum Role {
|
||||
ADMINISTRATOR
|
||||
COORDINATOR
|
||||
DRIVER
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VIP Management
|
||||
// ============================================
|
||||
|
||||
model VIP {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
organization String?
|
||||
department Department
|
||||
arrivalMode ArrivalMode
|
||||
expectedArrival DateTime? // For self-driving arrivals
|
||||
airportPickup Boolean @default(false)
|
||||
venueTransport Boolean @default(false)
|
||||
notes String? @db.Text
|
||||
flights Flight[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime? // Soft delete
|
||||
|
||||
@@map("vips")
|
||||
}
|
||||
|
||||
enum Department {
|
||||
OFFICE_OF_DEVELOPMENT
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum ArrivalMode {
|
||||
FLIGHT
|
||||
SELF_DRIVING
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Flight Tracking
|
||||
// ============================================
|
||||
|
||||
model Flight {
|
||||
id String @id @default(uuid())
|
||||
vipId String
|
||||
vip VIP @relation(fields: [vipId], references: [id], onDelete: Cascade)
|
||||
flightNumber String
|
||||
flightDate DateTime
|
||||
segment Int @default(1) // For multi-segment itineraries
|
||||
departureAirport String // IATA code (e.g., "JFK")
|
||||
arrivalAirport String // IATA code (e.g., "LAX")
|
||||
scheduledDeparture DateTime?
|
||||
scheduledArrival DateTime?
|
||||
actualDeparture DateTime?
|
||||
actualArrival DateTime?
|
||||
status String? // scheduled, delayed, landed, etc.
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("flights")
|
||||
@@index([vipId])
|
||||
@@index([flightNumber, flightDate])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Driver Management
|
||||
// ============================================
|
||||
|
||||
model Driver {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
phone String
|
||||
department Department?
|
||||
userId String? @unique
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
// Shift/Availability
|
||||
shiftStartTime DateTime? // When driver's shift starts
|
||||
shiftEndTime DateTime? // When driver's shift ends
|
||||
isAvailable Boolean @default(true)
|
||||
|
||||
events ScheduleEvent[]
|
||||
assignedVehicle Vehicle? @relation("AssignedDriver")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime? // Soft delete
|
||||
|
||||
@@map("drivers")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Vehicle Management
|
||||
// ============================================
|
||||
|
||||
model Vehicle {
|
||||
id String @id @default(uuid())
|
||||
name String // "Blue Van", "Suburban #3"
|
||||
type VehicleType @default(VAN)
|
||||
licensePlate String?
|
||||
seatCapacity Int // Total seats (e.g., 8)
|
||||
status VehicleStatus @default(AVAILABLE)
|
||||
|
||||
// Current assignment
|
||||
currentDriverId String? @unique
|
||||
currentDriver Driver? @relation("AssignedDriver", fields: [currentDriverId], references: [id])
|
||||
|
||||
// Relationships
|
||||
events ScheduleEvent[]
|
||||
|
||||
notes String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime? // Soft delete
|
||||
|
||||
@@map("vehicles")
|
||||
}
|
||||
|
||||
enum VehicleType {
|
||||
VAN // 7-15 seats
|
||||
SUV // 5-8 seats
|
||||
SEDAN // 4-5 seats
|
||||
BUS // 15+ seats
|
||||
GOLF_CART // 2-6 seats
|
||||
TRUCK // For equipment/supplies
|
||||
}
|
||||
|
||||
enum VehicleStatus {
|
||||
AVAILABLE // Ready to use
|
||||
IN_USE // Currently on a trip
|
||||
MAINTENANCE // Out of service
|
||||
RESERVED // Scheduled for upcoming trip
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Schedule & Event Management
|
||||
// ============================================
|
||||
|
||||
model ScheduleEvent {
|
||||
id String @id @default(uuid())
|
||||
vipIds String[] // Array of VIP IDs for multi-passenger trips
|
||||
title String
|
||||
|
||||
// Location details
|
||||
pickupLocation String?
|
||||
dropoffLocation String?
|
||||
location String? // For non-transport events
|
||||
|
||||
// Timing
|
||||
startTime DateTime
|
||||
endTime DateTime
|
||||
actualStartTime DateTime?
|
||||
actualEndTime DateTime?
|
||||
|
||||
description String? @db.Text
|
||||
type EventType @default(TRANSPORT)
|
||||
status EventStatus @default(SCHEDULED)
|
||||
|
||||
// Assignments
|
||||
driverId String?
|
||||
driver Driver? @relation(fields: [driverId], references: [id], onDelete: SetNull)
|
||||
vehicleId String?
|
||||
vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
|
||||
|
||||
// Metadata
|
||||
notes String? @db.Text
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime? // Soft delete
|
||||
|
||||
@@map("schedule_events")
|
||||
@@index([driverId])
|
||||
@@index([vehicleId])
|
||||
@@index([startTime, endTime])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
enum EventType {
|
||||
TRANSPORT
|
||||
MEETING
|
||||
EVENT
|
||||
MEAL
|
||||
ACCOMMODATION
|
||||
}
|
||||
|
||||
enum EventStatus {
|
||||
SCHEDULED
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
354
backend/prisma/seed.ts
Normal file
354
backend/prisma/seed.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
import { PrismaClient, Role, Department, ArrivalMode, EventType, EventStatus, VehicleType, VehicleStatus } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Seeding database...');
|
||||
|
||||
// Clean up existing data (careful in production!)
|
||||
await prisma.scheduleEvent.deleteMany({});
|
||||
await prisma.flight.deleteMany({});
|
||||
await prisma.vehicle.deleteMany({});
|
||||
await prisma.driver.deleteMany({});
|
||||
await prisma.vIP.deleteMany({});
|
||||
await prisma.user.deleteMany({});
|
||||
|
||||
console.log('✅ Cleared existing data');
|
||||
|
||||
// Create sample users
|
||||
const admin = await prisma.user.create({
|
||||
data: {
|
||||
auth0Id: 'auth0|admin-sample-id',
|
||||
email: 'admin@example.com',
|
||||
name: 'Admin User',
|
||||
role: Role.ADMINISTRATOR,
|
||||
isApproved: true,
|
||||
},
|
||||
});
|
||||
|
||||
const coordinator = await prisma.user.create({
|
||||
data: {
|
||||
auth0Id: 'auth0|coordinator-sample-id',
|
||||
email: 'coordinator@example.com',
|
||||
name: 'Coordinator User',
|
||||
role: Role.COORDINATOR,
|
||||
isApproved: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Note: test@test.com user is auto-created and auto-approved on first login (see auth.service.ts)
|
||||
|
||||
console.log('✅ Created sample users');
|
||||
|
||||
// Create sample vehicles with capacity
|
||||
const blackSUV = await prisma.vehicle.create({
|
||||
data: {
|
||||
name: 'Black Suburban',
|
||||
type: VehicleType.SUV,
|
||||
licensePlate: 'ABC-1234',
|
||||
seatCapacity: 6,
|
||||
status: VehicleStatus.AVAILABLE,
|
||||
notes: 'Leather interior, tinted windows',
|
||||
},
|
||||
});
|
||||
|
||||
const whiteVan = await prisma.vehicle.create({
|
||||
data: {
|
||||
name: 'White Sprinter Van',
|
||||
type: VehicleType.VAN,
|
||||
licensePlate: 'XYZ-5678',
|
||||
seatCapacity: 12,
|
||||
status: VehicleStatus.AVAILABLE,
|
||||
notes: 'High roof, wheelchair accessible',
|
||||
},
|
||||
});
|
||||
|
||||
const blueSedan = await prisma.vehicle.create({
|
||||
data: {
|
||||
name: 'Blue Camry',
|
||||
type: VehicleType.SEDAN,
|
||||
licensePlate: 'DEF-9012',
|
||||
seatCapacity: 4,
|
||||
status: VehicleStatus.AVAILABLE,
|
||||
notes: 'Fuel efficient, good for short trips',
|
||||
},
|
||||
});
|
||||
|
||||
const grayBus = await prisma.vehicle.create({
|
||||
data: {
|
||||
name: 'Gray Charter Bus',
|
||||
type: VehicleType.BUS,
|
||||
licensePlate: 'BUS-0001',
|
||||
seatCapacity: 40,
|
||||
status: VehicleStatus.AVAILABLE,
|
||||
notes: 'Full size charter bus, A/C, luggage compartment',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ Created sample vehicles with capacities');
|
||||
|
||||
// Create sample drivers
|
||||
const driver1 = await prisma.driver.create({
|
||||
data: {
|
||||
name: 'John Smith',
|
||||
phone: '+1 (555) 123-4567',
|
||||
department: Department.OFFICE_OF_DEVELOPMENT,
|
||||
},
|
||||
});
|
||||
|
||||
const driver2 = await prisma.driver.create({
|
||||
data: {
|
||||
name: 'Jane Doe',
|
||||
phone: '+1 (555) 987-6543',
|
||||
department: Department.ADMIN,
|
||||
},
|
||||
});
|
||||
|
||||
const driver3 = await prisma.driver.create({
|
||||
data: {
|
||||
name: 'Amanda Washington',
|
||||
phone: '+1 (555) 234-5678',
|
||||
department: Department.OFFICE_OF_DEVELOPMENT,
|
||||
},
|
||||
});
|
||||
|
||||
const driver4 = await prisma.driver.create({
|
||||
data: {
|
||||
name: 'Michael Thompson',
|
||||
phone: '+1 (555) 876-5432',
|
||||
department: Department.ADMIN,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ Created sample drivers');
|
||||
|
||||
// Create sample VIPs
|
||||
const vip1 = await prisma.vIP.create({
|
||||
data: {
|
||||
name: 'Dr. Robert Johnson',
|
||||
organization: 'Tech Corporation',
|
||||
department: Department.OFFICE_OF_DEVELOPMENT,
|
||||
arrivalMode: ArrivalMode.FLIGHT,
|
||||
airportPickup: true,
|
||||
venueTransport: true,
|
||||
notes: 'Prefers window seat, dietary restriction: vegetarian',
|
||||
flights: {
|
||||
create: [
|
||||
{
|
||||
flightNumber: 'AA123',
|
||||
flightDate: new Date('2026-02-15'),
|
||||
segment: 1,
|
||||
departureAirport: 'JFK',
|
||||
arrivalAirport: 'LAX',
|
||||
scheduledDeparture: new Date('2026-02-15T08:00:00'),
|
||||
scheduledArrival: new Date('2026-02-15T11:30:00'),
|
||||
status: 'scheduled',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const vip2 = await prisma.vIP.create({
|
||||
data: {
|
||||
name: 'Ms. Sarah Williams',
|
||||
organization: 'Global Foundation',
|
||||
department: Department.ADMIN,
|
||||
arrivalMode: ArrivalMode.SELF_DRIVING,
|
||||
expectedArrival: new Date('2026-02-16T14:00:00'),
|
||||
airportPickup: false,
|
||||
venueTransport: true,
|
||||
notes: 'Bringing assistant',
|
||||
},
|
||||
});
|
||||
|
||||
const vip3 = await prisma.vIP.create({
|
||||
data: {
|
||||
name: 'Emily Richardson (Harvard University)',
|
||||
organization: 'Harvard University',
|
||||
department: Department.OFFICE_OF_DEVELOPMENT,
|
||||
arrivalMode: ArrivalMode.FLIGHT,
|
||||
airportPickup: true,
|
||||
venueTransport: true,
|
||||
notes: 'Board member, requires accessible vehicle',
|
||||
},
|
||||
});
|
||||
|
||||
const vip4 = await prisma.vIP.create({
|
||||
data: {
|
||||
name: 'David Chen (Stanford)',
|
||||
organization: 'Stanford University',
|
||||
department: Department.OFFICE_OF_DEVELOPMENT,
|
||||
arrivalMode: ArrivalMode.FLIGHT,
|
||||
airportPickup: true,
|
||||
venueTransport: true,
|
||||
notes: 'Keynote speaker',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ Created sample VIPs');
|
||||
|
||||
// Create sample schedule events (unified activities) - NOW WITH MULTIPLE VIPS!
|
||||
|
||||
// Multi-VIP rideshare to Campfire Night (3 VIPs in one SUV)
|
||||
await prisma.scheduleEvent.create({
|
||||
data: {
|
||||
vipIds: [vip3.id, vip4.id, vip1.id], // 3 VIPs sharing a ride
|
||||
title: 'Transport to Campfire Night',
|
||||
pickupLocation: 'Grand Hotel Lobby',
|
||||
dropoffLocation: 'Camp Amphitheater',
|
||||
startTime: new Date('2026-02-15T19:45:00'),
|
||||
endTime: new Date('2026-02-15T20:00:00'),
|
||||
description: 'Rideshare: Emily, David, and Dr. Johnson to campfire',
|
||||
type: EventType.TRANSPORT,
|
||||
status: EventStatus.SCHEDULED,
|
||||
driverId: driver3.id,
|
||||
vehicleId: blackSUV.id, // 3 VIPs in 6-seat SUV (3/6 seats used)
|
||||
},
|
||||
});
|
||||
|
||||
// Single VIP transport
|
||||
await prisma.scheduleEvent.create({
|
||||
data: {
|
||||
vipIds: [vip1.id],
|
||||
title: 'Airport Pickup - Dr. Johnson',
|
||||
pickupLocation: 'LAX Terminal 4',
|
||||
dropoffLocation: 'Grand Hotel',
|
||||
startTime: new Date('2026-02-15T11:30:00'),
|
||||
endTime: new Date('2026-02-15T12:30:00'),
|
||||
description: 'Pick up Dr. Johnson from LAX',
|
||||
type: EventType.TRANSPORT,
|
||||
status: EventStatus.SCHEDULED,
|
||||
driverId: driver1.id,
|
||||
vehicleId: blueSedan.id, // 1 VIP in 4-seat sedan (1/4 seats used)
|
||||
},
|
||||
});
|
||||
|
||||
// Two VIPs sharing lunch transport
|
||||
await prisma.scheduleEvent.create({
|
||||
data: {
|
||||
vipIds: [vip1.id, vip2.id],
|
||||
title: 'Transport to Lunch - Day 1',
|
||||
pickupLocation: 'Grand Hotel Lobby',
|
||||
dropoffLocation: 'Main Dining Hall',
|
||||
startTime: new Date('2026-02-15T11:45:00'),
|
||||
endTime: new Date('2026-02-15T12:00:00'),
|
||||
description: 'Rideshare: Dr. Johnson and Ms. Williams to lunch',
|
||||
type: EventType.TRANSPORT,
|
||||
status: EventStatus.SCHEDULED,
|
||||
driverId: driver2.id,
|
||||
vehicleId: blueSedan.id, // 2 VIPs in 4-seat sedan (2/4 seats used)
|
||||
},
|
||||
});
|
||||
|
||||
// Large group transport in van
|
||||
await prisma.scheduleEvent.create({
|
||||
data: {
|
||||
vipIds: [vip1.id, vip2.id, vip3.id, vip4.id],
|
||||
title: 'Morning Shuttle to Conference',
|
||||
pickupLocation: 'Grand Hotel Lobby',
|
||||
dropoffLocation: 'Conference Center',
|
||||
startTime: new Date('2026-02-15T08:00:00'),
|
||||
endTime: new Date('2026-02-15T08:30:00'),
|
||||
description: 'All VIPs to morning conference session',
|
||||
type: EventType.TRANSPORT,
|
||||
status: EventStatus.SCHEDULED,
|
||||
driverId: driver4.id,
|
||||
vehicleId: whiteVan.id, // 4 VIPs in 12-seat van (4/12 seats used)
|
||||
},
|
||||
});
|
||||
|
||||
// Non-transport activities (unified system)
|
||||
|
||||
// Opening Ceremony - all VIPs attending
|
||||
await prisma.scheduleEvent.create({
|
||||
data: {
|
||||
vipIds: [vip1.id, vip2.id, vip3.id, vip4.id],
|
||||
title: 'Opening Ceremony',
|
||||
location: 'Main Stage',
|
||||
startTime: new Date('2026-02-15T10:00:00'),
|
||||
endTime: new Date('2026-02-15T11:30:00'),
|
||||
description: 'Welcome and opening remarks',
|
||||
type: EventType.EVENT,
|
||||
status: EventStatus.SCHEDULED,
|
||||
},
|
||||
});
|
||||
|
||||
// Lunch - Day 1 (all VIPs)
|
||||
await prisma.scheduleEvent.create({
|
||||
data: {
|
||||
vipIds: [vip1.id, vip2.id, vip3.id, vip4.id],
|
||||
title: 'Lunch - Day 1',
|
||||
location: 'Main Dining Hall',
|
||||
startTime: new Date('2026-02-15T12:00:00'),
|
||||
endTime: new Date('2026-02-15T13:30:00'),
|
||||
description: 'Day 1 lunch for all attendees',
|
||||
type: EventType.MEAL,
|
||||
status: EventStatus.SCHEDULED,
|
||||
},
|
||||
});
|
||||
|
||||
// Campfire Night (all VIPs)
|
||||
await prisma.scheduleEvent.create({
|
||||
data: {
|
||||
vipIds: [vip1.id, vip2.id, vip3.id, vip4.id],
|
||||
title: 'Campfire Night',
|
||||
location: 'Camp Amphitheater',
|
||||
startTime: new Date('2026-02-15T20:00:00'),
|
||||
endTime: new Date('2026-02-15T22:00:00'),
|
||||
description: 'Evening campfire and networking',
|
||||
type: EventType.EVENT,
|
||||
status: EventStatus.SCHEDULED,
|
||||
},
|
||||
});
|
||||
|
||||
// Private meeting - just Dr. Johnson and Ms. Williams
|
||||
await prisma.scheduleEvent.create({
|
||||
data: {
|
||||
vipIds: [vip1.id, vip2.id],
|
||||
title: 'Donor Meeting',
|
||||
location: 'Conference Room A',
|
||||
startTime: new Date('2026-02-15T14:00:00'),
|
||||
endTime: new Date('2026-02-15T15:00:00'),
|
||||
description: 'Private meeting with development team',
|
||||
type: EventType.MEETING,
|
||||
status: EventStatus.SCHEDULED,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ Created sample schedule events with multi-VIP rideshares and activities');
|
||||
|
||||
console.log('\n🎉 Database seeded successfully!');
|
||||
console.log('\nSample Users:');
|
||||
console.log('- Admin: admin@example.com');
|
||||
console.log('- Coordinator: coordinator@example.com');
|
||||
console.log('\nSample VIPs:');
|
||||
console.log('- Dr. Robert Johnson (Flight arrival)');
|
||||
console.log('- Ms. Sarah Williams (Self-driving)');
|
||||
console.log('- Emily Richardson (Harvard University)');
|
||||
console.log('- David Chen (Stanford)');
|
||||
console.log('\nSample Drivers:');
|
||||
console.log('- John Smith');
|
||||
console.log('- Jane Doe');
|
||||
console.log('- Amanda Washington');
|
||||
console.log('- Michael Thompson');
|
||||
console.log('\nSample Vehicles:');
|
||||
console.log('- Black Suburban (SUV, 6 seats)');
|
||||
console.log('- White Sprinter Van (Van, 12 seats)');
|
||||
console.log('- Blue Camry (Sedan, 4 seats)');
|
||||
console.log('- Gray Charter Bus (Bus, 40 seats)');
|
||||
console.log('\nSchedule Tasks (Multi-VIP Examples):');
|
||||
console.log('- 3 VIPs sharing SUV to Campfire (3/6 seats)');
|
||||
console.log('- 2 VIPs sharing sedan to Lunch (2/4 seats)');
|
||||
console.log('- 4 VIPs in van to Conference (4/12 seats)');
|
||||
console.log('- 1 VIP solo in sedan from Airport (1/4 seats)');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ Error seeding database:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -1,148 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VIP Coordinator API Documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
body {
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
.swagger-ui .topbar {
|
||||
background-color: #3498db;
|
||||
}
|
||||
.swagger-ui .topbar .download-url-wrapper .select-label {
|
||||
color: white;
|
||||
}
|
||||
.swagger-ui .topbar .download-url-wrapper input[type=text] {
|
||||
border: 2px solid #2980b9;
|
||||
}
|
||||
.swagger-ui .info .title {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.custom-header {
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.custom-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
font-weight: 300;
|
||||
}
|
||||
.custom-header p {
|
||||
margin: 10px 0 0 0;
|
||||
font-size: 1.2em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.quick-links {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.quick-links h3 {
|
||||
color: #2c3e50;
|
||||
margin-top: 0;
|
||||
}
|
||||
.quick-links ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.quick-links li {
|
||||
background: #ecf0f1;
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
.quick-links li strong {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.quick-links li code {
|
||||
background: #34495e;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="custom-header">
|
||||
<h1>🚗 VIP Coordinator API</h1>
|
||||
<p>Comprehensive API for managing VIP transportation coordination</p>
|
||||
</div>
|
||||
|
||||
<div class="quick-links">
|
||||
<h3>🚀 Quick Start Examples</h3>
|
||||
<ul>
|
||||
<li><strong>Health Check:</strong> <code>GET /api/health</code></li>
|
||||
<li><strong>Get All VIPs:</strong> <code>GET /api/vips</code></li>
|
||||
<li><strong>Get All Drivers:</strong> <code>GET /api/drivers</code></li>
|
||||
<li><strong>Flight Info:</strong> <code>GET /api/flights/UA1234?date=2025-06-26</code></li>
|
||||
<li><strong>VIP Schedule:</strong> <code>GET /api/vips/{vipId}/schedule</code></li>
|
||||
<li><strong>Driver Availability:</strong> <code>POST /api/drivers/availability</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: 'http://localhost:3000/api-documentation.yaml',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout",
|
||||
tryItOutEnabled: true,
|
||||
requestInterceptor: function(request) {
|
||||
// Add base URL if not present
|
||||
if (request.url.startsWith('/api/')) {
|
||||
request.url = 'http://localhost:3000' + request.url;
|
||||
}
|
||||
return request;
|
||||
},
|
||||
onComplete: function() {
|
||||
console.log('VIP Coordinator API Documentation loaded successfully!');
|
||||
},
|
||||
docExpansion: 'list',
|
||||
defaultModelsExpandDepth: 2,
|
||||
defaultModelExpandDepth: 2,
|
||||
showExtensions: true,
|
||||
showCommonExtensions: true,
|
||||
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch'],
|
||||
validatorUrl: null
|
||||
});
|
||||
// End Swagger UI call region
|
||||
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
14
backend/src/app.controller.ts
Normal file
14
backend/src/app.controller.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { Public } from './auth/decorators/public.decorator';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get('health')
|
||||
@Public() // Health check should be public
|
||||
getHealth() {
|
||||
return this.appService.getHealth();
|
||||
}
|
||||
}
|
||||
46
backend/src/app.module.ts
Normal file
46
backend/src/app.module.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { VipsModule } from './vips/vips.module';
|
||||
import { DriversModule } from './drivers/drivers.module';
|
||||
import { VehiclesModule } from './vehicles/vehicles.module';
|
||||
import { EventsModule } from './events/events.module';
|
||||
import { FlightsModule } from './flights/flights.module';
|
||||
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// Load environment variables
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
|
||||
// Core modules
|
||||
PrismaModule,
|
||||
AuthModule,
|
||||
|
||||
// Feature modules
|
||||
UsersModule,
|
||||
VipsModule,
|
||||
DriversModule,
|
||||
VehiclesModule,
|
||||
EventsModule,
|
||||
FlightsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
AppService,
|
||||
// Apply JWT auth guard globally (unless @Public() is used)
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
14
backend/src/app.service.ts
Normal file
14
backend/src/app.service.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHealth() {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'VIP Coordinator API',
|
||||
version: '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
};
|
||||
}
|
||||
}
|
||||
88
backend/src/auth/abilities/ability.factory.ts
Normal file
88
backend/src/auth/abilities/ability.factory.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { AbilityBuilder, PureAbility, AbilityClass, ExtractSubjectType, InferSubjects } from '@casl/ability';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Role, User, VIP, Driver, ScheduleEvent, Flight, Vehicle } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Define all possible actions in the system
|
||||
*/
|
||||
export enum Action {
|
||||
Manage = 'manage', // Special: allows everything
|
||||
Create = 'create',
|
||||
Read = 'read',
|
||||
Update = 'update',
|
||||
Delete = 'delete',
|
||||
Approve = 'approve', // Special: for user approval
|
||||
UpdateStatus = 'update-status', // Special: for drivers to update event status
|
||||
}
|
||||
|
||||
/**
|
||||
* Define all subjects (resources) in the system
|
||||
*/
|
||||
export type Subjects =
|
||||
| 'User'
|
||||
| 'VIP'
|
||||
| 'Driver'
|
||||
| 'ScheduleEvent'
|
||||
| 'Flight'
|
||||
| 'Vehicle'
|
||||
| 'all';
|
||||
|
||||
/**
|
||||
* Define the AppAbility type
|
||||
*/
|
||||
export type AppAbility = PureAbility<[Action, Subjects]>;
|
||||
|
||||
@Injectable()
|
||||
export class AbilityFactory {
|
||||
/**
|
||||
* Define abilities for a user based on their role
|
||||
*/
|
||||
defineAbilitiesFor(user: User): AppAbility {
|
||||
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
|
||||
PureAbility as AbilityClass<AppAbility>,
|
||||
);
|
||||
|
||||
// Define permissions based on role
|
||||
if (user.role === Role.ADMINISTRATOR) {
|
||||
// Administrators can do everything
|
||||
can(Action.Manage, 'all');
|
||||
} else if (user.role === Role.COORDINATOR) {
|
||||
// Coordinators have full access except user management
|
||||
can(Action.Read, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle']);
|
||||
can(Action.Create, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle']);
|
||||
can(Action.Update, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle']);
|
||||
can(Action.Delete, ['VIP', 'Driver', 'ScheduleEvent', 'Flight', 'Vehicle']);
|
||||
|
||||
// Cannot manage users
|
||||
cannot(Action.Create, 'User');
|
||||
cannot(Action.Update, 'User');
|
||||
cannot(Action.Delete, 'User');
|
||||
cannot(Action.Approve, 'User');
|
||||
} else if (user.role === Role.DRIVER) {
|
||||
// Drivers can only read most resources
|
||||
can(Action.Read, ['VIP', 'Driver', 'ScheduleEvent', 'Vehicle']);
|
||||
|
||||
// Drivers can update status of events (driver relationship checked in guard)
|
||||
can(Action.UpdateStatus, 'ScheduleEvent');
|
||||
|
||||
// Cannot access flights
|
||||
cannot(Action.Read, 'Flight');
|
||||
|
||||
// Cannot access users
|
||||
cannot(Action.Read, 'User');
|
||||
}
|
||||
|
||||
return build({
|
||||
// Detect subject type from string
|
||||
detectSubjectType: (item) => item as ExtractSubjectType<Subjects>,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can perform action on subject
|
||||
*/
|
||||
canUserPerform(user: User, action: Action, subject: Subjects): boolean {
|
||||
const ability = this.defineAbilitiesFor(user);
|
||||
return ability.can(action, subject);
|
||||
}
|
||||
}
|
||||
17
backend/src/auth/auth.controller.ts
Normal file
17
backend/src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { CurrentUser } from './decorators/current-user.decorator';
|
||||
import { User } from '@prisma/client';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Get('profile')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async getProfile(@CurrentUser() user: User) {
|
||||
// Return user profile (password already excluded by Prisma)
|
||||
return user;
|
||||
}
|
||||
}
|
||||
30
backend/src/auth/auth.module.ts
Normal file
30
backend/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { AbilityFactory } from './abilities/ability.factory';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
HttpModule,
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET') || 'development-secret-key',
|
||||
signOptions: {
|
||||
expiresIn: '7d',
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy, AbilityFactory],
|
||||
exports: [AuthService, PassportModule, JwtModule, AbilityFactory],
|
||||
})
|
||||
export class AuthModule {}
|
||||
70
backend/src/auth/auth.service.ts
Normal file
70
backend/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { Role } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Validate and get/create user from Auth0 token payload
|
||||
*/
|
||||
async validateUser(payload: any) {
|
||||
const namespace = 'https://vip-coordinator-api';
|
||||
const auth0Id = payload.sub;
|
||||
const email = payload[`${namespace}/email`] || payload.email || `${auth0Id}@auth0.local`;
|
||||
const name = payload[`${namespace}/name`] || payload.name || 'Unknown User';
|
||||
const picture = payload[`${namespace}/picture`] || payload.picture;
|
||||
|
||||
// Check if user exists
|
||||
let user = await this.prisma.user.findUnique({
|
||||
where: { auth0Id },
|
||||
include: { driver: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
// Check if this is the first user (auto-approve as admin)
|
||||
const approvedUserCount = await this.prisma.user.count({
|
||||
where: { isApproved: true, deletedAt: null },
|
||||
});
|
||||
const isFirstUser = approvedUserCount === 0;
|
||||
|
||||
this.logger.log(
|
||||
`Creating new user: ${email} (approvedUserCount: ${approvedUserCount}, isFirstUser: ${isFirstUser})`,
|
||||
);
|
||||
|
||||
// Create new user
|
||||
// First user is auto-approved as ADMINISTRATOR
|
||||
// Subsequent users default to DRIVER and require approval
|
||||
user = await this.prisma.user.create({
|
||||
data: {
|
||||
auth0Id,
|
||||
email,
|
||||
name,
|
||||
picture,
|
||||
role: isFirstUser ? Role.ADMINISTRATOR : Role.DRIVER,
|
||||
isApproved: isFirstUser, // Auto-approve first user only
|
||||
},
|
||||
include: { driver: true },
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`User created: ${user.email} with role ${user.role} (approved: ${user.isApproved})`,
|
||||
);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user profile
|
||||
*/
|
||||
async getCurrentUser(auth0Id: string) {
|
||||
return this.prisma.user.findUnique({
|
||||
where: { auth0Id },
|
||||
include: { driver: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
39
backend/src/auth/decorators/check-ability.decorator.ts
Normal file
39
backend/src/auth/decorators/check-ability.decorator.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Action, Subjects } from '../abilities/ability.factory';
|
||||
import { CHECK_ABILITY, RequiredPermission } from '../guards/abilities.guard';
|
||||
|
||||
/**
|
||||
* Decorator to check CASL abilities on a route
|
||||
*
|
||||
* @example
|
||||
* @CheckAbilities({ action: Action.Create, subject: 'VIP' })
|
||||
* async create(@Body() dto: CreateVIPDto) {
|
||||
* return this.service.create(dto);
|
||||
* }
|
||||
*
|
||||
* @example Multiple permissions (all must be satisfied)
|
||||
* @CheckAbilities(
|
||||
* { action: Action.Read, subject: 'VIP' },
|
||||
* { action: Action.Update, subject: 'VIP' }
|
||||
* )
|
||||
*/
|
||||
export const CheckAbilities = (...permissions: RequiredPermission[]) =>
|
||||
SetMetadata(CHECK_ABILITY, permissions);
|
||||
|
||||
/**
|
||||
* Helper functions for common permission checks
|
||||
*/
|
||||
export const CanCreate = (subject: Subjects) =>
|
||||
CheckAbilities({ action: Action.Create, subject });
|
||||
|
||||
export const CanRead = (subject: Subjects) =>
|
||||
CheckAbilities({ action: Action.Read, subject });
|
||||
|
||||
export const CanUpdate = (subject: Subjects) =>
|
||||
CheckAbilities({ action: Action.Update, subject });
|
||||
|
||||
export const CanDelete = (subject: Subjects) =>
|
||||
CheckAbilities({ action: Action.Delete, subject });
|
||||
|
||||
export const CanManage = (subject: Subjects) =>
|
||||
CheckAbilities({ action: Action.Manage, subject });
|
||||
8
backend/src/auth/decorators/current-user.decorator.ts
Normal file
8
backend/src/auth/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
4
backend/src/auth/decorators/public.decorator.ts
Normal file
4
backend/src/auth/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
5
backend/src/auth/decorators/roles.decorator.ts
Normal file
5
backend/src/auth/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Role } from '@prisma/client';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
||||
64
backend/src/auth/guards/abilities.guard.ts
Normal file
64
backend/src/auth/guards/abilities.guard.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AbilityFactory, Action, Subjects } from '../abilities/ability.factory';
|
||||
|
||||
/**
|
||||
* Interface for required permissions
|
||||
*/
|
||||
export interface RequiredPermission {
|
||||
action: Action;
|
||||
subject: Subjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata key for permissions
|
||||
*/
|
||||
export const CHECK_ABILITY = 'check_ability';
|
||||
|
||||
/**
|
||||
* Guard that checks CASL abilities
|
||||
*/
|
||||
@Injectable()
|
||||
export class AbilitiesGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private abilityFactory: AbilityFactory,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const requiredPermissions =
|
||||
this.reflector.get<RequiredPermission[]>(
|
||||
CHECK_ABILITY,
|
||||
context.getHandler(),
|
||||
) || [];
|
||||
|
||||
// If no permissions required, allow access
|
||||
if (requiredPermissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
// User should be attached by JwtAuthGuard
|
||||
if (!user) {
|
||||
throw new ForbiddenException('User not authenticated');
|
||||
}
|
||||
|
||||
// Build abilities for user
|
||||
const ability = this.abilityFactory.defineAbilitiesFor(user);
|
||||
|
||||
// Check if user has all required permissions
|
||||
const hasPermission = requiredPermissions.every((permission) =>
|
||||
ability.can(permission.action, permission.subject),
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new ForbiddenException(
|
||||
`User does not have required permissions`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
25
backend/src/auth/guards/jwt-auth.guard.ts
Normal file
25
backend/src/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
// Check if route is marked as public
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
23
backend/src/auth/guards/roles.guard.ts
Normal file
23
backend/src/auth/guards/roles.guard.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Role } from '@prisma/client';
|
||||
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!requiredRoles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
return requiredRoles.some((role) => user.role === role);
|
||||
}
|
||||
}
|
||||
75
backend/src/auth/strategies/jwt.strategy.ts
Normal file
75
backend/src/auth/strategies/jwt.strategy.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Injectable, UnauthorizedException, Logger } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Strategy, ExtractJwt } from 'passport-jwt';
|
||||
import { passportJwtSecret } from 'jwks-rsa';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
private readonly logger = new Logger(JwtStrategy.name);
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private authService: AuthService,
|
||||
private httpService: HttpService,
|
||||
) {
|
||||
super({
|
||||
secretOrKeyProvider: passportJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 5,
|
||||
jwksUri: `${configService.get('AUTH0_ISSUER')}.well-known/jwks.json`,
|
||||
}),
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
audience: configService.get('AUTH0_AUDIENCE'),
|
||||
issuer: configService.get('AUTH0_ISSUER'),
|
||||
algorithms: ['RS256'],
|
||||
passReqToCallback: true, // We need the request to get the token
|
||||
});
|
||||
}
|
||||
|
||||
async validate(req: any, payload: any) {
|
||||
// Extract token from Authorization header
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
// Fetch user info from Auth0 /userinfo endpoint
|
||||
try {
|
||||
const userInfoUrl = `${this.configService.get('AUTH0_ISSUER')}userinfo`;
|
||||
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get(userInfoUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Merge userinfo data into payload
|
||||
const userInfo = response.data;
|
||||
|
||||
payload.email = userInfo.email || payload.email;
|
||||
payload.name = userInfo.name || payload.name;
|
||||
payload.picture = userInfo.picture || payload.picture;
|
||||
payload.email_verified = userInfo.email_verified;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to fetch user info: ${error.message}`);
|
||||
// Continue with payload-only data (fallbacks will apply)
|
||||
}
|
||||
|
||||
// Get or create user from Auth0 token
|
||||
const user = await this.authService.validateUser(payload);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('User not found');
|
||||
}
|
||||
|
||||
if (!user.isApproved) {
|
||||
throw new UnauthorizedException('User account pending approval');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
63
backend/src/common/filters/all-exceptions.filter.ts
Normal file
63
backend/src/common/filters/all-exceptions.filter.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
/**
|
||||
* Catch-all exception filter for unhandled errors
|
||||
* This ensures all errors return a consistent format
|
||||
*/
|
||||
@Catch()
|
||||
export class AllExceptionsFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(AllExceptionsFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let message = 'Internal server error';
|
||||
let stack: string | undefined;
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
message =
|
||||
typeof exceptionResponse === 'string'
|
||||
? exceptionResponse
|
||||
: (exceptionResponse as any).message || exception.message;
|
||||
stack = exception.stack;
|
||||
} else if (exception instanceof Error) {
|
||||
message = exception.message;
|
||||
stack = exception.stack;
|
||||
}
|
||||
|
||||
const errorResponse = {
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
method: request.method,
|
||||
message,
|
||||
error: HttpStatus[status],
|
||||
};
|
||||
|
||||
// Log the error
|
||||
this.logger.error(
|
||||
`[${request.method}] ${request.url} - ${status} - ${message}`,
|
||||
stack,
|
||||
);
|
||||
|
||||
// In development, include stack trace in response
|
||||
if (process.env.NODE_ENV === 'development' && stack) {
|
||||
(errorResponse as any).stack = stack;
|
||||
}
|
||||
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
}
|
||||
88
backend/src/common/filters/http-exception.filter.ts
Normal file
88
backend/src/common/filters/http-exception.filter.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
/**
|
||||
* Global exception filter that catches all HTTP exceptions
|
||||
* and formats them consistently with proper logging
|
||||
*/
|
||||
@Catch(HttpException)
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(HttpExceptionFilter.name);
|
||||
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const status = exception.getStatus();
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
// Extract error details
|
||||
const errorDetails =
|
||||
typeof exceptionResponse === 'string'
|
||||
? { message: exceptionResponse }
|
||||
: (exceptionResponse as any);
|
||||
|
||||
// Build standardized error response
|
||||
const errorResponse = {
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
method: request.method,
|
||||
message: errorDetails.message || exception.message,
|
||||
error: errorDetails.error || HttpStatus[status],
|
||||
...(errorDetails.details && { details: errorDetails.details }),
|
||||
...(errorDetails.conflicts && { conflicts: errorDetails.conflicts }),
|
||||
};
|
||||
|
||||
// Log error with appropriate level
|
||||
const logMessage = `[${request.method}] ${request.url} - ${status} - ${errorResponse.message}`;
|
||||
|
||||
if (status >= 500) {
|
||||
this.logger.error(logMessage, exception.stack);
|
||||
} else if (status >= 400) {
|
||||
this.logger.warn(logMessage);
|
||||
} else {
|
||||
this.logger.log(logMessage);
|
||||
}
|
||||
|
||||
// Log request details for debugging (exclude sensitive data)
|
||||
if (status >= 400) {
|
||||
const sanitizedBody = this.sanitizeRequestBody(request.body);
|
||||
this.logger.debug(
|
||||
`Request details: ${JSON.stringify({
|
||||
params: request.params,
|
||||
query: request.query,
|
||||
body: sanitizedBody,
|
||||
user: (request as any).user?.email,
|
||||
})}`,
|
||||
);
|
||||
}
|
||||
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove sensitive fields from request body before logging
|
||||
*/
|
||||
private sanitizeRequestBody(body: any): any {
|
||||
if (!body) return body;
|
||||
|
||||
const sensitiveFields = ['password', 'token', 'apiKey', 'secret'];
|
||||
const sanitized = { ...body };
|
||||
|
||||
sensitiveFields.forEach((field) => {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '***REDACTED***';
|
||||
}
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
2
backend/src/common/filters/index.ts
Normal file
2
backend/src/common/filters/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './http-exception.filter';
|
||||
export * from './all-exceptions.filter';
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Pool } from 'pg';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:changeme@localhost:5432/vip_coordinator',
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
// Test the connection
|
||||
pool.on('connect', () => {
|
||||
console.log('✅ Connected to PostgreSQL database');
|
||||
});
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('❌ PostgreSQL connection error:', err);
|
||||
});
|
||||
|
||||
export default pool;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { createClient } from 'redis';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const redisClient = createClient({
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379'
|
||||
});
|
||||
|
||||
redisClient.on('connect', () => {
|
||||
console.log('✅ Connected to Redis');
|
||||
});
|
||||
|
||||
redisClient.on('error', (err: Error) => {
|
||||
console.error('❌ Redis connection error:', err);
|
||||
});
|
||||
|
||||
// Connect to Redis
|
||||
redisClient.connect().catch((err: Error) => {
|
||||
console.error('❌ Failed to connect to Redis:', err);
|
||||
});
|
||||
|
||||
export default redisClient;
|
||||
@@ -1,130 +0,0 @@
|
||||
-- VIP Coordinator Database Schema
|
||||
|
||||
-- Create VIPs table
|
||||
CREATE TABLE IF NOT EXISTS vips (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
organization VARCHAR(255) NOT NULL,
|
||||
department VARCHAR(255) DEFAULT 'Office of Development',
|
||||
transport_mode VARCHAR(50) NOT NULL CHECK (transport_mode IN ('flight', 'self-driving')),
|
||||
expected_arrival TIMESTAMP,
|
||||
needs_airport_pickup BOOLEAN DEFAULT false,
|
||||
needs_venue_transport BOOLEAN DEFAULT true,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create flights table (for VIPs with flight transport)
|
||||
CREATE TABLE IF NOT EXISTS flights (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
|
||||
flight_number VARCHAR(50) NOT NULL,
|
||||
flight_date DATE NOT NULL,
|
||||
segment INTEGER NOT NULL,
|
||||
departure_airport VARCHAR(10),
|
||||
arrival_airport VARCHAR(10),
|
||||
scheduled_departure TIMESTAMP,
|
||||
scheduled_arrival TIMESTAMP,
|
||||
actual_departure TIMESTAMP,
|
||||
actual_arrival TIMESTAMP,
|
||||
status VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create drivers table
|
||||
CREATE TABLE IF NOT EXISTS drivers (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(50) NOT NULL,
|
||||
department VARCHAR(255) DEFAULT 'Office of Development',
|
||||
user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create schedule_events table
|
||||
CREATE TABLE IF NOT EXISTS schedule_events (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
location VARCHAR(255) NOT NULL,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
description TEXT,
|
||||
assigned_driver_id VARCHAR(255) REFERENCES drivers(id) ON DELETE SET NULL,
|
||||
status VARCHAR(50) DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
|
||||
event_type VARCHAR(50) NOT NULL CHECK (event_type IN ('transport', 'meeting', 'event', 'meal', 'accommodation')),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create users table for authentication
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
google_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL CHECK (role IN ('driver', 'coordinator', 'administrator')),
|
||||
profile_picture_url TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create system_setup table for tracking initial setup
|
||||
CREATE TABLE IF NOT EXISTS system_setup (
|
||||
id SERIAL PRIMARY KEY,
|
||||
setup_completed BOOLEAN DEFAULT false,
|
||||
first_admin_created BOOLEAN DEFAULT false,
|
||||
setup_date TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create admin_settings table
|
||||
CREATE TABLE IF NOT EXISTS admin_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
setting_key VARCHAR(255) UNIQUE NOT NULL,
|
||||
setting_value TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_vips_transport_mode ON vips(transport_mode);
|
||||
CREATE INDEX IF NOT EXISTS idx_flights_vip_id ON flights(vip_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_flights_date ON flights(flight_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_events_vip_id ON schedule_events(vip_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_events_driver_id ON schedule_events(assigned_driver_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_events_start_time ON schedule_events(start_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_events_status ON schedule_events(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
|
||||
CREATE INDEX IF NOT EXISTS idx_drivers_user_id ON drivers(user_id);
|
||||
|
||||
-- Create updated_at trigger function
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Create triggers for updated_at (drop if exists first)
|
||||
DROP TRIGGER IF EXISTS update_vips_updated_at ON vips;
|
||||
DROP TRIGGER IF EXISTS update_flights_updated_at ON flights;
|
||||
DROP TRIGGER IF EXISTS update_drivers_updated_at ON drivers;
|
||||
DROP TRIGGER IF EXISTS update_schedule_events_updated_at ON schedule_events;
|
||||
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
|
||||
DROP TRIGGER IF EXISTS update_admin_settings_updated_at ON admin_settings;
|
||||
|
||||
CREATE TRIGGER update_vips_updated_at BEFORE UPDATE ON vips FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_flights_updated_at BEFORE UPDATE ON flights FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_drivers_updated_at BEFORE UPDATE ON drivers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_schedule_events_updated_at BEFORE UPDATE ON schedule_events FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_admin_settings_updated_at BEFORE UPDATE ON admin_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -1,134 +0,0 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
google_id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
profile_picture_url?: string;
|
||||
role: 'driver' | 'coordinator' | 'administrator';
|
||||
created_at?: string;
|
||||
last_login?: string;
|
||||
is_active?: boolean;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export function generateToken(user: User): string {
|
||||
return jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
google_id: user.google_id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
profile_picture_url: user.profile_picture_url,
|
||||
role: user.role
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): User | null {
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
return {
|
||||
id: decoded.id,
|
||||
google_id: decoded.google_id,
|
||||
email: decoded.email,
|
||||
name: decoded.name,
|
||||
profile_picture_url: decoded.profile_picture_url,
|
||||
role: decoded.role
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple Google OAuth2 client using fetch
|
||||
export async function verifyGoogleToken(googleToken: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(`https://www.googleapis.com/oauth2/v1/userinfo?access_token=${googleToken}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Invalid Google token');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error verifying Google token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get Google OAuth2 URL
|
||||
export function getGoogleAuthUrl(): string {
|
||||
const clientId = process.env.GOOGLE_CLIENT_ID;
|
||||
const redirectUri = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/auth/google/callback';
|
||||
|
||||
if (!clientId) {
|
||||
throw new Error('GOOGLE_CLIENT_ID not configured');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
scope: 'openid email profile',
|
||||
access_type: 'offline',
|
||||
prompt: 'consent'
|
||||
});
|
||||
|
||||
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
export async function exchangeCodeForTokens(code: string): Promise<any> {
|
||||
const clientId = process.env.GOOGLE_CLIENT_ID;
|
||||
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
||||
const redirectUri = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/auth/google/callback';
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error('Google OAuth credentials not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to exchange code for tokens');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error exchanging code for tokens:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get user info from Google
|
||||
export async function getGoogleUserInfo(accessToken: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(`https://www.googleapis.com/oauth2/v2/userinfo?access_token=${accessToken}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get user info');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error getting Google user info:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
63
backend/src/drivers/drivers.controller.ts
Normal file
63
backend/src/drivers/drivers.controller.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { DriversService } from './drivers.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { Role } from '@prisma/client';
|
||||
import { CreateDriverDto, UpdateDriverDto } from './dto';
|
||||
|
||||
@Controller('drivers')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class DriversController {
|
||||
constructor(private readonly driversService: DriversService) {}
|
||||
|
||||
@Post()
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
create(@Body() createDriverDto: CreateDriverDto) {
|
||||
return this.driversService.create(createDriverDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR, Role.DRIVER)
|
||||
findAll() {
|
||||
return this.driversService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR, Role.DRIVER)
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.driversService.findOne(id);
|
||||
}
|
||||
|
||||
@Get(':id/schedule')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR, Role.DRIVER)
|
||||
getSchedule(@Param('id') id: string) {
|
||||
return this.driversService.getSchedule(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
update(@Param('id') id: string, @Body() updateDriverDto: UpdateDriverDto) {
|
||||
return this.driversService.update(id, updateDriverDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
remove(
|
||||
@Param('id') id: string,
|
||||
@Query('hard') hard?: string,
|
||||
) {
|
||||
const isHardDelete = hard === 'true';
|
||||
return this.driversService.remove(id, isHardDelete);
|
||||
}
|
||||
}
|
||||
10
backend/src/drivers/drivers.module.ts
Normal file
10
backend/src/drivers/drivers.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DriversController } from './drivers.controller';
|
||||
import { DriversService } from './drivers.service';
|
||||
|
||||
@Module({
|
||||
controllers: [DriversController],
|
||||
providers: [DriversService],
|
||||
exports: [DriversService],
|
||||
})
|
||||
export class DriversModule {}
|
||||
89
backend/src/drivers/drivers.service.ts
Normal file
89
backend/src/drivers/drivers.service.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateDriverDto, UpdateDriverDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class DriversService {
|
||||
private readonly logger = new Logger(DriversService.name);
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(createDriverDto: CreateDriverDto) {
|
||||
this.logger.log(`Creating driver: ${createDriverDto.name}`);
|
||||
|
||||
return this.prisma.driver.create({
|
||||
data: createDriverDto,
|
||||
include: { user: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.driver.findMany({
|
||||
where: { deletedAt: null },
|
||||
include: {
|
||||
user: true,
|
||||
events: {
|
||||
where: { deletedAt: null },
|
||||
include: { vehicle: true, driver: true },
|
||||
orderBy: { startTime: 'asc' },
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const driver = await this.prisma.driver.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
include: {
|
||||
user: true,
|
||||
events: {
|
||||
where: { deletedAt: null },
|
||||
include: { vehicle: true, driver: true },
|
||||
orderBy: { startTime: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!driver) {
|
||||
throw new NotFoundException(`Driver with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return driver;
|
||||
}
|
||||
|
||||
async update(id: string, updateDriverDto: UpdateDriverDto) {
|
||||
const driver = await this.findOne(id);
|
||||
|
||||
this.logger.log(`Updating driver ${id}: ${driver.name}`);
|
||||
|
||||
return this.prisma.driver.update({
|
||||
where: { id: driver.id },
|
||||
data: updateDriverDto,
|
||||
include: { user: true },
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: string, hardDelete = false) {
|
||||
const driver = await this.findOne(id);
|
||||
|
||||
if (hardDelete) {
|
||||
this.logger.log(`Hard deleting driver: ${driver.name}`);
|
||||
return this.prisma.driver.delete({
|
||||
where: { id: driver.id },
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(`Soft deleting driver: ${driver.name}`);
|
||||
return this.prisma.driver.update({
|
||||
where: { id: driver.id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
async getSchedule(id: string) {
|
||||
const driver = await this.findOne(id);
|
||||
|
||||
return driver.events;
|
||||
}
|
||||
}
|
||||
18
backend/src/drivers/dto/create-driver.dto.ts
Normal file
18
backend/src/drivers/dto/create-driver.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IsString, IsEnum, IsOptional, IsUUID } from 'class-validator';
|
||||
import { Department } from '@prisma/client';
|
||||
|
||||
export class CreateDriverDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
phone: string;
|
||||
|
||||
@IsEnum(Department)
|
||||
@IsOptional()
|
||||
department?: Department;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
userId?: string;
|
||||
}
|
||||
2
backend/src/drivers/dto/index.ts
Normal file
2
backend/src/drivers/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './create-driver.dto';
|
||||
export * from './update-driver.dto';
|
||||
4
backend/src/drivers/dto/update-driver.dto.ts
Normal file
4
backend/src/drivers/dto/update-driver.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateDriverDto } from './create-driver.dto';
|
||||
|
||||
export class UpdateDriverDto extends PartialType(CreateDriverDto) {}
|
||||
16
backend/src/events/dto/add-vips-to-event.dto.ts
Normal file
16
backend/src/events/dto/add-vips-to-event.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { IsArray, IsUUID, IsString, IsOptional, IsInt, Min } from 'class-validator';
|
||||
|
||||
export class AddVipsToEventDto {
|
||||
@IsArray()
|
||||
@IsUUID('4', { each: true })
|
||||
vipIds: string[];
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
pickupMinutesBeforeEvent?: number; // How many minutes before event should pickup happen (default: 15)
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
pickupLocationOverride?: string; // Override default pickup location
|
||||
}
|
||||
58
backend/src/events/dto/create-event.dto.ts
Normal file
58
backend/src/events/dto/create-event.dto.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
IsString,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
IsDateString,
|
||||
} from 'class-validator';
|
||||
import { EventType, EventStatus } from '@prisma/client';
|
||||
|
||||
export class CreateEventDto {
|
||||
@IsUUID('4', { each: true })
|
||||
vipIds: string[]; // Array of VIP IDs for multi-passenger trips
|
||||
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
location?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
pickupLocation?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
dropoffLocation?: string;
|
||||
|
||||
@IsDateString()
|
||||
startTime: string;
|
||||
|
||||
@IsDateString()
|
||||
endTime: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
notes?: string;
|
||||
|
||||
@IsEnum(EventType)
|
||||
@IsOptional()
|
||||
type?: EventType;
|
||||
|
||||
@IsEnum(EventStatus)
|
||||
@IsOptional()
|
||||
status?: EventStatus;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
driverId?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
vehicleId?: string;
|
||||
}
|
||||
4
backend/src/events/dto/index.ts
Normal file
4
backend/src/events/dto/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './create-event.dto';
|
||||
export * from './update-event.dto';
|
||||
export * from './update-event-status.dto';
|
||||
export * from './add-vips-to-event.dto';
|
||||
7
backend/src/events/dto/update-event-status.dto.ts
Normal file
7
backend/src/events/dto/update-event-status.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { IsEnum } from 'class-validator';
|
||||
import { EventStatus } from '@prisma/client';
|
||||
|
||||
export class UpdateEventStatusDto {
|
||||
@IsEnum(EventStatus)
|
||||
status: EventStatus;
|
||||
}
|
||||
9
backend/src/events/dto/update-event.dto.ts
Normal file
9
backend/src/events/dto/update-event.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { IsBoolean, IsOptional } from 'class-validator';
|
||||
import { CreateEventDto } from './create-event.dto';
|
||||
|
||||
export class UpdateEventDto extends PartialType(CreateEventDto) {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
forceAssign?: boolean; // Allow double-booking drivers with confirmation
|
||||
}
|
||||
66
backend/src/events/events.controller.ts
Normal file
66
backend/src/events/events.controller.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { EventsService } from './events.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { Role } from '@prisma/client';
|
||||
import { CreateEventDto, UpdateEventDto, UpdateEventStatusDto } from './dto';
|
||||
|
||||
@Controller('events')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class EventsController {
|
||||
constructor(private readonly eventsService: EventsService) {}
|
||||
|
||||
@Post()
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
create(@Body() createEventDto: CreateEventDto) {
|
||||
return this.eventsService.create(createEventDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR, Role.DRIVER)
|
||||
findAll() {
|
||||
return this.eventsService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR, Role.DRIVER)
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.eventsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
update(@Param('id') id: string, @Body() updateEventDto: UpdateEventDto) {
|
||||
return this.eventsService.update(id, updateEventDto);
|
||||
}
|
||||
|
||||
@Patch(':id/status')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR, Role.DRIVER)
|
||||
updateStatus(
|
||||
@Param('id') id: string,
|
||||
@Body() updateEventStatusDto: UpdateEventStatusDto,
|
||||
) {
|
||||
return this.eventsService.updateStatus(id, updateEventStatusDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
remove(
|
||||
@Param('id') id: string,
|
||||
@Query('hard') hard?: string,
|
||||
) {
|
||||
const isHardDelete = hard === 'true';
|
||||
return this.eventsService.remove(id, isHardDelete);
|
||||
}
|
||||
}
|
||||
16
backend/src/events/events.module.ts
Normal file
16
backend/src/events/events.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EventsController } from './events.controller';
|
||||
import { EventsService } from './events.service';
|
||||
|
||||
@Module({
|
||||
controllers: [
|
||||
EventsController,
|
||||
],
|
||||
providers: [
|
||||
EventsService,
|
||||
],
|
||||
exports: [
|
||||
EventsService,
|
||||
],
|
||||
})
|
||||
export class EventsModule {}
|
||||
318
backend/src/events/events.service.ts
Normal file
318
backend/src/events/events.service.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateEventDto, UpdateEventDto, UpdateEventStatusDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class EventsService {
|
||||
private readonly logger = new Logger(EventsService.name);
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(createEventDto: CreateEventDto) {
|
||||
this.logger.log(`Creating event: ${createEventDto.title}`);
|
||||
|
||||
// Validate VIPs exist
|
||||
if (createEventDto.vipIds && createEventDto.vipIds.length > 0) {
|
||||
const vips = await this.prisma.vIP.findMany({
|
||||
where: {
|
||||
id: { in: createEventDto.vipIds },
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (vips.length !== createEventDto.vipIds.length) {
|
||||
throw new BadRequestException('One or more VIPs not found');
|
||||
}
|
||||
}
|
||||
|
||||
// Check vehicle capacity if vehicle is assigned
|
||||
if (createEventDto.vehicleId && createEventDto.vipIds) {
|
||||
await this.checkVehicleCapacity(
|
||||
createEventDto.vehicleId,
|
||||
createEventDto.vipIds.length,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for conflicts if driver is assigned
|
||||
if (createEventDto.driverId) {
|
||||
const conflicts = await this.checkConflicts(
|
||||
createEventDto.driverId,
|
||||
new Date(createEventDto.startTime),
|
||||
new Date(createEventDto.endTime),
|
||||
);
|
||||
|
||||
if (conflicts.length > 0) {
|
||||
this.logger.warn(
|
||||
`Conflict detected for driver ${createEventDto.driverId}`,
|
||||
);
|
||||
throw new BadRequestException({
|
||||
message: 'Driver has conflicting events',
|
||||
conflicts: conflicts.map((e) => ({
|
||||
id: e.id,
|
||||
title: e.title,
|
||||
startTime: e.startTime,
|
||||
endTime: e.endTime,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const event = await this.prisma.scheduleEvent.create({
|
||||
data: {
|
||||
...createEventDto,
|
||||
startTime: new Date(createEventDto.startTime),
|
||||
endTime: new Date(createEventDto.endTime),
|
||||
},
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
},
|
||||
});
|
||||
|
||||
return this.enrichEventWithVips(event);
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
const events = await this.prisma.scheduleEvent.findMany({
|
||||
where: { deletedAt: null },
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
},
|
||||
orderBy: { startTime: 'asc' },
|
||||
});
|
||||
|
||||
return Promise.all(events.map((event) => this.enrichEventWithVips(event)));
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const event = await this.prisma.scheduleEvent.findFirst({
|
||||
where: { id, deletedAt: null },
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new NotFoundException(`Event with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return this.enrichEventWithVips(event);
|
||||
}
|
||||
|
||||
async update(id: string, updateEventDto: UpdateEventDto) {
|
||||
const event = await this.findOne(id);
|
||||
|
||||
// Validate VIPs if being updated
|
||||
if (updateEventDto.vipIds && updateEventDto.vipIds.length > 0) {
|
||||
const vips = await this.prisma.vIP.findMany({
|
||||
where: {
|
||||
id: { in: updateEventDto.vipIds },
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (vips.length !== updateEventDto.vipIds.length) {
|
||||
throw new BadRequestException('One or more VIPs not found');
|
||||
}
|
||||
}
|
||||
|
||||
// Check vehicle capacity if vehicle or VIPs are being updated
|
||||
const vehicleId = updateEventDto.vehicleId || event.vehicleId;
|
||||
const vipCount = updateEventDto.vipIds
|
||||
? updateEventDto.vipIds.length
|
||||
: event.vipIds.length;
|
||||
|
||||
if (vehicleId && vipCount > 0 && !updateEventDto.forceAssign) {
|
||||
await this.checkVehicleCapacity(vehicleId, vipCount);
|
||||
}
|
||||
|
||||
// Check for conflicts if driver or times are being updated (unless forceAssign is true)
|
||||
if (
|
||||
!updateEventDto.forceAssign &&
|
||||
(updateEventDto.driverId ||
|
||||
updateEventDto.startTime ||
|
||||
updateEventDto.endTime)
|
||||
) {
|
||||
const driverId = updateEventDto.driverId || event.driverId;
|
||||
const startTime = updateEventDto.startTime
|
||||
? new Date(updateEventDto.startTime)
|
||||
: event.startTime;
|
||||
const endTime = updateEventDto.endTime
|
||||
? new Date(updateEventDto.endTime)
|
||||
: event.endTime;
|
||||
|
||||
if (driverId) {
|
||||
const conflicts = await this.checkConflicts(
|
||||
driverId,
|
||||
startTime,
|
||||
endTime,
|
||||
event.id, // Exclude current event from conflict check
|
||||
);
|
||||
|
||||
if (conflicts.length > 0) {
|
||||
this.logger.warn(`Conflict detected for driver ${driverId}`);
|
||||
throw new BadRequestException({
|
||||
message: 'Driver has conflicting events',
|
||||
conflicts: conflicts.map((e) => ({
|
||||
id: e.id,
|
||||
title: e.title,
|
||||
startTime: e.startTime,
|
||||
endTime: e.endTime,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Updating event ${id}: ${event.title}`);
|
||||
|
||||
const updateData: any = { ...updateEventDto };
|
||||
if (updateEventDto.startTime) {
|
||||
updateData.startTime = new Date(updateEventDto.startTime);
|
||||
}
|
||||
if (updateEventDto.endTime) {
|
||||
updateData.endTime = new Date(updateEventDto.endTime);
|
||||
}
|
||||
|
||||
// Remove forceAssign from data as it's not a database field
|
||||
delete updateData.forceAssign;
|
||||
|
||||
const updatedEvent = await this.prisma.scheduleEvent.update({
|
||||
where: { id: event.id },
|
||||
data: updateData,
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
},
|
||||
});
|
||||
|
||||
return this.enrichEventWithVips(updatedEvent);
|
||||
}
|
||||
|
||||
async updateStatus(id: string, updateEventStatusDto: UpdateEventStatusDto) {
|
||||
const event = await this.findOne(id);
|
||||
|
||||
this.logger.log(
|
||||
`Updating event status ${id}: ${event.title} -> ${updateEventStatusDto.status}`,
|
||||
);
|
||||
|
||||
const updatedEvent = await this.prisma.scheduleEvent.update({
|
||||
where: { id: event.id },
|
||||
data: { status: updateEventStatusDto.status },
|
||||
include: {
|
||||
driver: true,
|
||||
vehicle: true,
|
||||
},
|
||||
});
|
||||
|
||||
return this.enrichEventWithVips(updatedEvent);
|
||||
}
|
||||
|
||||
async remove(id: string, hardDelete = false) {
|
||||
const event = await this.findOne(id);
|
||||
|
||||
if (hardDelete) {
|
||||
this.logger.log(`Hard deleting event: ${event.title}`);
|
||||
return this.prisma.scheduleEvent.delete({
|
||||
where: { id: event.id },
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log(`Soft deleting event: ${event.title}`);
|
||||
return this.prisma.scheduleEvent.update({
|
||||
where: { id: event.id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check vehicle capacity
|
||||
*/
|
||||
private async checkVehicleCapacity(vehicleId: string, vipCount: number) {
|
||||
const vehicle = await this.prisma.vehicle.findFirst({
|
||||
where: { id: vehicleId, deletedAt: null },
|
||||
});
|
||||
|
||||
if (!vehicle) {
|
||||
throw new NotFoundException('Vehicle not found');
|
||||
}
|
||||
|
||||
if (vipCount > vehicle.seatCapacity) {
|
||||
this.logger.warn(
|
||||
`Vehicle capacity exceeded: ${vipCount} VIPs > ${vehicle.seatCapacity} seats`,
|
||||
);
|
||||
throw new BadRequestException({
|
||||
message: `Vehicle capacity exceeded: ${vipCount} VIPs require more than ${vehicle.seatCapacity} available seats`,
|
||||
capacity: vehicle.seatCapacity,
|
||||
requested: vipCount,
|
||||
exceeded: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for conflicting events for a driver
|
||||
*/
|
||||
private async checkConflicts(
|
||||
driverId: string,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
excludeEventId?: string,
|
||||
) {
|
||||
return this.prisma.scheduleEvent.findMany({
|
||||
where: {
|
||||
driverId,
|
||||
deletedAt: null,
|
||||
id: excludeEventId ? { not: excludeEventId } : undefined,
|
||||
OR: [
|
||||
{
|
||||
// New event starts during existing event
|
||||
AND: [
|
||||
{ startTime: { lte: startTime } },
|
||||
{ endTime: { gt: startTime } },
|
||||
],
|
||||
},
|
||||
{
|
||||
// New event ends during existing event
|
||||
AND: [
|
||||
{ startTime: { lt: endTime } },
|
||||
{ endTime: { gte: endTime } },
|
||||
],
|
||||
},
|
||||
{
|
||||
// New event completely contains existing event
|
||||
AND: [
|
||||
{ startTime: { gte: startTime } },
|
||||
{ endTime: { lte: endTime } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich event with VIP details fetched separately
|
||||
*/
|
||||
private async enrichEventWithVips(event: any) {
|
||||
if (!event.vipIds || event.vipIds.length === 0) {
|
||||
return { ...event, vips: [] };
|
||||
}
|
||||
|
||||
const vips = await this.prisma.vIP.findMany({
|
||||
where: {
|
||||
id: { in: event.vipIds },
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
return { ...event, vips };
|
||||
}
|
||||
}
|
||||
42
backend/src/flights/dto/create-flight.dto.ts
Normal file
42
backend/src/flights/dto/create-flight.dto.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { IsString, IsDateString, IsInt, IsUUID, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateFlightDto {
|
||||
@IsUUID()
|
||||
vipId: string;
|
||||
|
||||
@IsString()
|
||||
flightNumber: string;
|
||||
|
||||
@IsDateString()
|
||||
flightDate: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
segment?: number;
|
||||
|
||||
@IsString()
|
||||
departureAirport: string;
|
||||
|
||||
@IsString()
|
||||
arrivalAirport: string;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
scheduledDeparture?: string;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
scheduledArrival?: string;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
actualDeparture?: string;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
actualArrival?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
status?: string;
|
||||
}
|
||||
2
backend/src/flights/dto/index.ts
Normal file
2
backend/src/flights/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './create-flight.dto';
|
||||
export * from './update-flight.dto';
|
||||
4
backend/src/flights/dto/update-flight.dto.ts
Normal file
4
backend/src/flights/dto/update-flight.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateFlightDto } from './create-flight.dto';
|
||||
|
||||
export class UpdateFlightDto extends PartialType(CreateFlightDto) {}
|
||||
72
backend/src/flights/flights.controller.ts
Normal file
72
backend/src/flights/flights.controller.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { FlightsService } from './flights.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/guards/roles.guard';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { Role } from '@prisma/client';
|
||||
import { CreateFlightDto, UpdateFlightDto } from './dto';
|
||||
|
||||
@Controller('flights')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class FlightsController {
|
||||
constructor(private readonly flightsService: FlightsService) {}
|
||||
|
||||
@Post()
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
create(@Body() createFlightDto: CreateFlightDto) {
|
||||
return this.flightsService.create(createFlightDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
findAll() {
|
||||
return this.flightsService.findAll();
|
||||
}
|
||||
|
||||
@Get('status/:flightNumber')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
getFlightStatus(
|
||||
@Param('flightNumber') flightNumber: string,
|
||||
@Query('date') date?: string,
|
||||
) {
|
||||
return this.flightsService.getFlightStatus(flightNumber, date);
|
||||
}
|
||||
|
||||
@Get('vip/:vipId')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
findByVip(@Param('vipId') vipId: string) {
|
||||
return this.flightsService.findByVip(vipId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.flightsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
update(@Param('id') id: string, @Body() updateFlightDto: UpdateFlightDto) {
|
||||
return this.flightsService.update(id, updateFlightDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles(Role.ADMINISTRATOR, Role.COORDINATOR)
|
||||
remove(
|
||||
@Param('id') id: string,
|
||||
@Query('hard') hard?: string,
|
||||
) {
|
||||
const isHardDelete = hard === 'true';
|
||||
return this.flightsService.remove(id, isHardDelete);
|
||||
}
|
||||
}
|
||||
12
backend/src/flights/flights.module.ts
Normal file
12
backend/src/flights/flights.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { FlightsController } from './flights.controller';
|
||||
import { FlightsService } from './flights.service';
|
||||
|
||||
@Module({
|
||||
imports: [HttpModule],
|
||||
controllers: [FlightsController],
|
||||
providers: [FlightsService],
|
||||
exports: [FlightsService],
|
||||
})
|
||||
export class FlightsModule {}
|
||||
170
backend/src/flights/flights.service.ts
Normal file
170
backend/src/flights/flights.service.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateFlightDto, UpdateFlightDto } from './dto';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class FlightsService {
|
||||
private readonly logger = new Logger(FlightsService.name);
|
||||
private readonly apiKey: string;
|
||||
private readonly baseUrl = 'http://api.aviationstack.com/v1';
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private httpService: HttpService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.apiKey = this.configService.get('AVIATIONSTACK_API_KEY') || '';
|
||||
}
|
||||
|
||||
async create(createFlightDto: CreateFlightDto) {
|
||||
this.logger.log(
|
||||
`Creating flight: ${createFlightDto.flightNumber} for VIP ${createFlightDto.vipId}`,
|
||||
);
|
||||
|
||||
return this.prisma.flight.create({
|
||||
data: {
|
||||
...createFlightDto,
|
||||
flightDate: new Date(createFlightDto.flightDate),
|
||||
scheduledDeparture: createFlightDto.scheduledDeparture
|
||||
? new Date(createFlightDto.scheduledDeparture)
|
||||
: undefined,
|
||||
scheduledArrival: createFlightDto.scheduledArrival
|
||||
? new Date(createFlightDto.scheduledArrival)
|
||||
: undefined,
|
||||
},
|
||||
include: { vip: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.flight.findMany({
|
||||
include: { vip: true },
|
||||
orderBy: { flightDate: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findByVip(vipId: string) {
|
||||
return this.prisma.flight.findMany({
|
||||
where: { vipId },
|
||||
orderBy: [{ flightDate: 'asc' }, { segment: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const flight = await this.prisma.flight.findUnique({
|
||||
where: { id },
|
||||
include: { vip: true },
|
||||
});
|
||||
|
||||
if (!flight) {
|
||||
throw new NotFoundException(`Flight with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return flight;
|
||||
}
|
||||
|
||||
async update(id: string, updateFlightDto: UpdateFlightDto) {
|
||||
const flight = await this.findOne(id);
|
||||
|
||||
this.logger.log(`Updating flight ${id}: ${flight.flightNumber}`);
|
||||
|
||||
const updateData: any = { ...updateFlightDto };
|
||||
const dto = updateFlightDto as any; // Type assertion to work around PartialType
|
||||
|
||||
if (dto.flightDate) {
|
||||
updateData.flightDate = new Date(dto.flightDate);
|
||||
}
|
||||
if (dto.scheduledDeparture) {
|
||||
updateData.scheduledDeparture = new Date(dto.scheduledDeparture);
|
||||
}
|
||||
if (dto.scheduledArrival) {
|
||||
updateData.scheduledArrival = new Date(dto.scheduledArrival);
|
||||
}
|
||||
if (dto.actualDeparture) {
|
||||
updateData.actualDeparture = new Date(dto.actualDeparture);
|
||||
}
|
||||
if (dto.actualArrival) {
|
||||
updateData.actualArrival = new Date(dto.actualArrival);
|
||||
}
|
||||
|
||||
return this.prisma.flight.update({
|
||||
where: { id: flight.id },
|
||||
data: updateData,
|
||||
include: { vip: true },
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: string, hardDelete = false) {
|
||||
const flight = await this.findOne(id);
|
||||
|
||||
this.logger.log(`Deleting flight: ${flight.flightNumber}`);
|
||||
|
||||
// Flights are always hard deleted (no soft delete for flights)
|
||||
return this.prisma.flight.delete({
|
||||
where: { id: flight.id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch real-time flight status from AviationStack API
|
||||
*/
|
||||
async getFlightStatus(flightNumber: string, flightDate?: string) {
|
||||
if (!this.apiKey) {
|
||||
this.logger.warn('AviationStack API key not configured');
|
||||
return {
|
||||
message: 'Flight tracking API not configured',
|
||||
flightNumber,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
access_key: this.apiKey,
|
||||
flight_iata: flightNumber,
|
||||
};
|
||||
|
||||
if (flightDate) {
|
||||
params.flight_date = flightDate;
|
||||
}
|
||||
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get(`${this.baseUrl}/flights`, { params }),
|
||||
);
|
||||
|
||||
const data = response.data as any;
|
||||
if (data && data.data && data.data.length > 0) {
|
||||
const flightData = data.data[0];
|
||||
|
||||
return {
|
||||
flightNumber: flightData.flight.iata,
|
||||
status: flightData.flight_status,
|
||||
departure: {
|
||||
airport: flightData.departure.iata,
|
||||
scheduled: flightData.departure.scheduled,
|
||||
actual: flightData.departure.actual,
|
||||
},
|
||||
arrival: {
|
||||
airport: flightData.arrival.iata,
|
||||
scheduled: flightData.arrival.scheduled,
|
||||
estimated: flightData.arrival.estimated,
|
||||
actual: flightData.arrival.actual,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Flight not found',
|
||||
flightNumber,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to fetch flight status: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,769 +0,0 @@
|
||||
import express, { Express, Request, Response } from 'express';
|
||||
import dotenv from 'dotenv';
|
||||
import cors from 'cors';
|
||||
import authRoutes, { requireAuth, requireRole } from './routes/simpleAuth';
|
||||
import flightService from './services/flightService';
|
||||
import driverConflictService from './services/driverConflictService';
|
||||
import scheduleValidationService from './services/scheduleValidationService';
|
||||
import FlightTrackingScheduler from './services/flightTrackingScheduler';
|
||||
import enhancedDataService from './services/enhancedDataService';
|
||||
import databaseService from './services/databaseService';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app: Express = express();
|
||||
const port: number = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
||||
|
||||
// Middleware
|
||||
app.use(cors({
|
||||
origin: [
|
||||
process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||
'https://bsa.madeamess.online:5173',
|
||||
'https://bsa.madeamess.online',
|
||||
'https://api.bsa.madeamess.online',
|
||||
'http://bsa.madeamess.online:5173'
|
||||
],
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Simple JWT-based authentication - no passport needed
|
||||
|
||||
// Authentication routes
|
||||
app.use('/auth', authRoutes);
|
||||
|
||||
// Temporary admin bypass route (remove after setup)
|
||||
app.get('/admin-bypass', (req: Request, res: Response) => {
|
||||
res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:5173'}/admin?bypass=true`);
|
||||
});
|
||||
|
||||
// Serve static files from public directory
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req: Request, res: Response) => {
|
||||
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Data is now persisted using dataService - no more in-memory storage!
|
||||
|
||||
// Simple admin password (in production, use proper auth)
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
|
||||
// Initialize flight tracking scheduler
|
||||
const flightTracker = new FlightTrackingScheduler(flightService);
|
||||
|
||||
// VIP routes (protected)
|
||||
app.post('/api/vips', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
|
||||
// Create a new VIP
|
||||
const {
|
||||
name,
|
||||
organization,
|
||||
department, // New: Office of Development or Admin
|
||||
transportMode,
|
||||
flightNumber, // Legacy single flight
|
||||
flights, // New: array of flights
|
||||
expectedArrival,
|
||||
needsAirportPickup,
|
||||
needsVenueTransport,
|
||||
notes
|
||||
} = req.body;
|
||||
|
||||
const newVip = {
|
||||
id: Date.now().toString(), // Simple ID generation
|
||||
name,
|
||||
organization,
|
||||
department: department || 'Office of Development', // Default to Office of Development
|
||||
transportMode: transportMode || 'flight',
|
||||
// Support both legacy single flight and new multiple flights
|
||||
flightNumber: transportMode === 'flight' && !flights ? flightNumber : undefined,
|
||||
flights: transportMode === 'flight' && flights ? flights : undefined,
|
||||
expectedArrival: transportMode === 'self-driving' ? expectedArrival : undefined,
|
||||
arrivalTime: transportMode === 'flight' ? undefined : expectedArrival, // Legacy field for flight arrivals
|
||||
needsAirportPickup: transportMode === 'flight' ? (needsAirportPickup !== false) : false,
|
||||
needsVenueTransport: needsVenueTransport !== false, // Default to true
|
||||
assignedDriverIds: [],
|
||||
notes: notes || '',
|
||||
schedule: []
|
||||
};
|
||||
|
||||
const savedVip = await enhancedDataService.addVip(newVip);
|
||||
|
||||
// Add flights to tracking scheduler if applicable
|
||||
if (savedVip.transportMode === 'flight' && savedVip.flights && savedVip.flights.length > 0) {
|
||||
flightTracker.addVipFlights(savedVip.id, savedVip.name, savedVip.flights);
|
||||
}
|
||||
|
||||
res.status(201).json(savedVip);
|
||||
});
|
||||
|
||||
app.get('/api/vips', requireAuth, async (req: Request, res: Response) => {
|
||||
try {
|
||||
// Fetch all VIPs
|
||||
const vips = await enhancedDataService.getVips();
|
||||
res.json(vips);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch VIPs' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/vips/:id', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
|
||||
// Update a VIP
|
||||
const { id } = req.params;
|
||||
const {
|
||||
name,
|
||||
organization,
|
||||
department, // New: Office of Development or Admin
|
||||
transportMode,
|
||||
flightNumber, // Legacy single flight
|
||||
flights, // New: array of flights
|
||||
expectedArrival,
|
||||
needsAirportPickup,
|
||||
needsVenueTransport,
|
||||
notes
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
const updatedVip = {
|
||||
name,
|
||||
organization,
|
||||
department: department || 'Office of Development',
|
||||
transportMode: transportMode || 'flight',
|
||||
// Support both legacy single flight and new multiple flights
|
||||
flights: transportMode === 'flight' && flights ? flights : undefined,
|
||||
expectedArrival: transportMode === 'self-driving' ? expectedArrival : undefined,
|
||||
needsAirportPickup: transportMode === 'flight' ? (needsAirportPickup !== false) : false,
|
||||
needsVenueTransport: needsVenueTransport !== false,
|
||||
notes: notes || ''
|
||||
};
|
||||
|
||||
const savedVip = await enhancedDataService.updateVip(id, updatedVip);
|
||||
|
||||
if (!savedVip) {
|
||||
return res.status(404).json({ error: 'VIP not found' });
|
||||
}
|
||||
|
||||
// Update flight tracking if needed
|
||||
if (savedVip.transportMode === 'flight') {
|
||||
// Remove old flights
|
||||
flightTracker.removeVipFlights(id);
|
||||
|
||||
// Add new flights if any
|
||||
if (savedVip.flights && savedVip.flights.length > 0) {
|
||||
flightTracker.addVipFlights(savedVip.id, savedVip.name, savedVip.flights);
|
||||
}
|
||||
}
|
||||
|
||||
res.json(savedVip);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update VIP' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/vips/:id', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
|
||||
// Delete a VIP
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
const deletedVip = await enhancedDataService.deleteVip(id);
|
||||
|
||||
if (!deletedVip) {
|
||||
return res.status(404).json({ error: 'VIP not found' });
|
||||
}
|
||||
|
||||
// Remove from flight tracking
|
||||
flightTracker.removeVipFlights(id);
|
||||
|
||||
res.json({ message: 'VIP deleted successfully', vip: deletedVip });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete VIP' });
|
||||
}
|
||||
});
|
||||
|
||||
// Driver routes (protected)
|
||||
app.post('/api/drivers', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
|
||||
// Create a new driver
|
||||
const { name, phone, currentLocation, department } = req.body;
|
||||
|
||||
const newDriver = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
phone,
|
||||
department: department || 'Office of Development', // Default to Office of Development
|
||||
currentLocation: currentLocation || { lat: 0, lng: 0 },
|
||||
assignedVipIds: []
|
||||
};
|
||||
|
||||
try {
|
||||
const savedDriver = await enhancedDataService.addDriver(newDriver);
|
||||
res.status(201).json(savedDriver);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create driver' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/drivers', requireAuth, async (req: Request, res: Response) => {
|
||||
try {
|
||||
// Fetch all drivers
|
||||
const drivers = await enhancedDataService.getDrivers();
|
||||
res.json(drivers);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch drivers' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/drivers/:id', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
|
||||
// Update a driver
|
||||
const { id } = req.params;
|
||||
const { name, phone, currentLocation, department } = req.body;
|
||||
|
||||
try {
|
||||
const updatedDriver = {
|
||||
name,
|
||||
phone,
|
||||
department: department || 'Office of Development',
|
||||
currentLocation: currentLocation || { lat: 0, lng: 0 }
|
||||
};
|
||||
|
||||
const savedDriver = await enhancedDataService.updateDriver(id, updatedDriver);
|
||||
|
||||
if (!savedDriver) {
|
||||
return res.status(404).json({ error: 'Driver not found' });
|
||||
}
|
||||
|
||||
res.json(savedDriver);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update driver' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/drivers/:id', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
|
||||
// Delete a driver
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
const deletedDriver = await enhancedDataService.deleteDriver(id);
|
||||
|
||||
if (!deletedDriver) {
|
||||
return res.status(404).json({ error: 'Driver not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Driver deleted successfully', driver: deletedDriver });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete driver' });
|
||||
}
|
||||
});
|
||||
|
||||
// Enhanced flight tracking routes with date specificity
|
||||
app.get('/api/flights/:flightNumber', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { flightNumber } = req.params;
|
||||
const { date, departureAirport, arrivalAirport } = req.query;
|
||||
|
||||
// Default to today if no date provided
|
||||
const flightDate = (date as string) || new Date().toISOString().split('T')[0];
|
||||
|
||||
const flightData = await flightService.getFlightInfo({
|
||||
flightNumber,
|
||||
date: flightDate,
|
||||
departureAirport: departureAirport as string,
|
||||
arrivalAirport: arrivalAirport as string
|
||||
});
|
||||
|
||||
if (flightData) {
|
||||
// Always return flight data for validation, even if date doesn't match
|
||||
res.json(flightData);
|
||||
} else {
|
||||
// Only return 404 if the flight number itself is invalid
|
||||
res.status(404).json({ error: 'Invalid flight number - this flight does not exist' });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch flight data' });
|
||||
}
|
||||
});
|
||||
|
||||
// Start periodic updates for a flight
|
||||
app.post('/api/flights/:flightNumber/track', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { flightNumber } = req.params;
|
||||
const { date, intervalMinutes = 5 } = req.body;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({ error: 'Flight date is required' });
|
||||
}
|
||||
|
||||
flightService.startPeriodicUpdates({
|
||||
flightNumber,
|
||||
date
|
||||
}, intervalMinutes);
|
||||
|
||||
res.json({ message: `Started tracking ${flightNumber} on ${date}` });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to start flight tracking' });
|
||||
}
|
||||
});
|
||||
|
||||
// Stop periodic updates for a flight
|
||||
app.delete('/api/flights/:flightNumber/track', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { flightNumber } = req.params;
|
||||
const { date } = req.query;
|
||||
|
||||
if (!date) {
|
||||
return res.status(400).json({ error: 'Flight date is required' });
|
||||
}
|
||||
|
||||
const key = `${flightNumber}_${date}`;
|
||||
flightService.stopPeriodicUpdates(key);
|
||||
|
||||
res.json({ message: `Stopped tracking ${flightNumber} on ${date}` });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to stop flight tracking' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/flights/batch', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { flights } = req.body;
|
||||
|
||||
if (!Array.isArray(flights)) {
|
||||
return res.status(400).json({ error: 'flights must be an array of {flightNumber, date} objects' });
|
||||
}
|
||||
|
||||
// Validate flight objects
|
||||
for (const flight of flights) {
|
||||
if (!flight.flightNumber || !flight.date) {
|
||||
return res.status(400).json({ error: 'Each flight must have flightNumber and date' });
|
||||
}
|
||||
}
|
||||
|
||||
const flightData = await flightService.getMultipleFlights(flights);
|
||||
res.json(flightData);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch flight data' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get flight tracking status
|
||||
app.get('/api/flights/tracking/status', (req: Request, res: Response) => {
|
||||
const status = flightTracker.getTrackingStatus();
|
||||
res.json(status);
|
||||
});
|
||||
|
||||
// Schedule management routes (protected)
|
||||
app.get('/api/vips/:vipId/schedule', requireAuth, async (req: Request, res: Response) => {
|
||||
const { vipId } = req.params;
|
||||
try {
|
||||
const vipSchedule = await enhancedDataService.getSchedule(vipId);
|
||||
res.json(vipSchedule);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch schedule' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/vips/:vipId/schedule', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
|
||||
const { vipId } = req.params;
|
||||
const { title, location, startTime, endTime, description, type, assignedDriverId } = req.body;
|
||||
|
||||
// Validate the event
|
||||
const validationErrors = scheduleValidationService.validateEvent({
|
||||
title: title || '',
|
||||
location: location || '',
|
||||
startTime: startTime || '',
|
||||
endTime: endTime || '',
|
||||
type: type || ''
|
||||
}, false);
|
||||
|
||||
const { critical, warnings } = scheduleValidationService.categorizeErrors(validationErrors);
|
||||
|
||||
// Return validation errors if any critical errors exist
|
||||
if (critical.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
validationErrors: critical,
|
||||
warnings: warnings,
|
||||
message: scheduleValidationService.getErrorSummary(critical)
|
||||
});
|
||||
}
|
||||
|
||||
const newEvent = {
|
||||
id: Date.now().toString(),
|
||||
title,
|
||||
location,
|
||||
startTime,
|
||||
endTime,
|
||||
description: description || '',
|
||||
assignedDriverId: assignedDriverId || '',
|
||||
status: 'scheduled',
|
||||
type
|
||||
};
|
||||
|
||||
try {
|
||||
const savedEvent = await enhancedDataService.addScheduleEvent(vipId, newEvent);
|
||||
|
||||
// Include warnings in the response if any
|
||||
const response: any = { ...savedEvent };
|
||||
if (warnings.length > 0) {
|
||||
response.warnings = warnings;
|
||||
}
|
||||
|
||||
res.status(201).json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create schedule event' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/vips/:vipId/schedule/:eventId', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
|
||||
const { vipId, eventId } = req.params;
|
||||
const { title, location, startTime, endTime, description, type, assignedDriverId, status } = req.body;
|
||||
|
||||
// Validate the updated event (with edit flag for grace period)
|
||||
const validationErrors = scheduleValidationService.validateEvent({
|
||||
title: title || '',
|
||||
location: location || '',
|
||||
startTime: startTime || '',
|
||||
endTime: endTime || '',
|
||||
type: type || ''
|
||||
}, true);
|
||||
|
||||
const { critical, warnings } = scheduleValidationService.categorizeErrors(validationErrors);
|
||||
|
||||
// Return validation errors if any critical errors exist
|
||||
if (critical.length > 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
validationErrors: critical,
|
||||
warnings: warnings,
|
||||
message: scheduleValidationService.getErrorSummary(critical)
|
||||
});
|
||||
}
|
||||
|
||||
const updatedEvent = {
|
||||
id: eventId,
|
||||
title,
|
||||
location,
|
||||
startTime,
|
||||
endTime,
|
||||
description: description || '',
|
||||
assignedDriverId: assignedDriverId || '',
|
||||
type,
|
||||
status: status || 'scheduled'
|
||||
};
|
||||
|
||||
try {
|
||||
const savedEvent = await enhancedDataService.updateScheduleEvent(vipId, eventId, updatedEvent);
|
||||
|
||||
if (!savedEvent) {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
|
||||
// Include warnings in the response if any
|
||||
const response: any = { ...savedEvent };
|
||||
if (warnings.length > 0) {
|
||||
response.warnings = warnings;
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update schedule event' });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/vips/:vipId/schedule/:eventId/status', requireAuth, async (req: Request, res: Response) => {
|
||||
const { vipId, eventId } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
try {
|
||||
const currentSchedule = await enhancedDataService.getSchedule(vipId);
|
||||
const currentEvent = currentSchedule.find((event: any) => event.id === eventId);
|
||||
|
||||
if (!currentEvent) {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
|
||||
const updatedEvent = { ...currentEvent, status };
|
||||
const savedEvent = await enhancedDataService.updateScheduleEvent(vipId, eventId, updatedEvent);
|
||||
|
||||
if (!savedEvent) {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
|
||||
res.json(savedEvent);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update event status' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/vips/:vipId/schedule/:eventId', requireAuth, requireRole(['coordinator', 'administrator']), async (req: Request, res: Response) => {
|
||||
const { vipId, eventId } = req.params;
|
||||
|
||||
try {
|
||||
const deletedEvent = await enhancedDataService.deleteScheduleEvent(vipId, eventId);
|
||||
|
||||
if (!deletedEvent) {
|
||||
return res.status(404).json({ error: 'Event not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Event deleted successfully', event: deletedEvent });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete schedule event' });
|
||||
}
|
||||
});
|
||||
|
||||
// Driver availability and conflict checking (protected)
|
||||
app.post('/api/drivers/availability', requireAuth, async (req: Request, res: Response) => {
|
||||
const { startTime, endTime, location } = req.body;
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
return res.status(400).json({ error: 'startTime and endTime are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const allSchedules = await enhancedDataService.getAllSchedules();
|
||||
const drivers = await enhancedDataService.getDrivers();
|
||||
|
||||
const availability = driverConflictService.getDriverAvailability(
|
||||
{ startTime, endTime, location: location || '' },
|
||||
allSchedules as any,
|
||||
drivers
|
||||
);
|
||||
|
||||
res.json(availability);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to check driver availability' });
|
||||
}
|
||||
});
|
||||
|
||||
// Check conflicts for specific driver assignment (protected)
|
||||
app.post('/api/drivers/:driverId/conflicts', requireAuth, async (req: Request, res: Response) => {
|
||||
const { driverId } = req.params;
|
||||
const { startTime, endTime, location } = req.body;
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
return res.status(400).json({ error: 'startTime and endTime are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const allSchedules = await enhancedDataService.getAllSchedules();
|
||||
const drivers = await enhancedDataService.getDrivers();
|
||||
|
||||
const conflicts = driverConflictService.checkDriverConflicts(
|
||||
driverId,
|
||||
{ startTime, endTime, location: location || '' },
|
||||
allSchedules as any,
|
||||
drivers
|
||||
);
|
||||
|
||||
res.json({ conflicts });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to check driver conflicts' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get driver's complete schedule (protected)
|
||||
app.get('/api/drivers/:driverId/schedule', requireAuth, async (req: Request, res: Response) => {
|
||||
const { driverId } = req.params;
|
||||
|
||||
try {
|
||||
const drivers = await enhancedDataService.getDrivers();
|
||||
const driver = drivers.find((d: any) => d.id === driverId);
|
||||
if (!driver) {
|
||||
return res.status(404).json({ error: 'Driver not found' });
|
||||
}
|
||||
|
||||
// Get all events assigned to this driver across all VIPs
|
||||
const driverSchedule: any[] = [];
|
||||
const allSchedules = await enhancedDataService.getAllSchedules();
|
||||
const vips = await enhancedDataService.getVips();
|
||||
|
||||
Object.entries(allSchedules).forEach(([vipId, events]: [string, any]) => {
|
||||
events.forEach((event: any) => {
|
||||
if (event.assignedDriverId === driverId) {
|
||||
// Get VIP name
|
||||
const vip = vips.find((v: any) => v.id === vipId);
|
||||
driverSchedule.push({
|
||||
...event,
|
||||
vipId,
|
||||
vipName: vip ? vip.name : 'Unknown VIP'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by start time
|
||||
driverSchedule.sort((a, b) =>
|
||||
new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
|
||||
);
|
||||
|
||||
res.json({
|
||||
driver: {
|
||||
id: driver.id,
|
||||
name: driver.name,
|
||||
phone: driver.phone,
|
||||
department: driver.department
|
||||
},
|
||||
schedule: driverSchedule
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch driver schedule' });
|
||||
}
|
||||
});
|
||||
|
||||
// Admin routes
|
||||
app.post('/api/admin/authenticate', (req: Request, res: Response) => {
|
||||
const { password } = req.body;
|
||||
|
||||
if (password === ADMIN_PASSWORD) {
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(401).json({ error: 'Invalid password' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/admin/settings', async (req: Request, res: Response) => {
|
||||
const adminAuth = req.headers['admin-auth'];
|
||||
|
||||
if (adminAuth !== 'true') {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const adminSettings = await enhancedDataService.getAdminSettings();
|
||||
|
||||
// Return settings but mask API keys for display only
|
||||
// IMPORTANT: Don't return the actual keys, just indicate they exist
|
||||
const maskedSettings = {
|
||||
apiKeys: {
|
||||
aviationStackKey: adminSettings.apiKeys.aviationStackKey ? '***' + adminSettings.apiKeys.aviationStackKey.slice(-4) : '',
|
||||
googleMapsKey: adminSettings.apiKeys.googleMapsKey ? '***' + adminSettings.apiKeys.googleMapsKey.slice(-4) : '',
|
||||
twilioKey: adminSettings.apiKeys.twilioKey ? '***' + adminSettings.apiKeys.twilioKey.slice(-4) : '',
|
||||
googleClientId: adminSettings.apiKeys.googleClientId ? '***' + adminSettings.apiKeys.googleClientId.slice(-4) : '',
|
||||
googleClientSecret: adminSettings.apiKeys.googleClientSecret ? '***' + adminSettings.apiKeys.googleClientSecret.slice(-4) : ''
|
||||
},
|
||||
systemSettings: adminSettings.systemSettings
|
||||
};
|
||||
|
||||
res.json(maskedSettings);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch admin settings' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/admin/settings', async (req: Request, res: Response) => {
|
||||
const adminAuth = req.headers['admin-auth'];
|
||||
|
||||
if (adminAuth !== 'true') {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
try {
|
||||
const { apiKeys, systemSettings } = req.body;
|
||||
const currentSettings = await enhancedDataService.getAdminSettings();
|
||||
|
||||
// Update API keys (only if provided and not masked)
|
||||
if (apiKeys) {
|
||||
if (apiKeys.aviationStackKey && !apiKeys.aviationStackKey.startsWith('***')) {
|
||||
currentSettings.apiKeys.aviationStackKey = apiKeys.aviationStackKey;
|
||||
// Update the environment variable for the flight service
|
||||
process.env.AVIATIONSTACK_API_KEY = apiKeys.aviationStackKey;
|
||||
}
|
||||
if (apiKeys.googleMapsKey && !apiKeys.googleMapsKey.startsWith('***')) {
|
||||
currentSettings.apiKeys.googleMapsKey = apiKeys.googleMapsKey;
|
||||
}
|
||||
if (apiKeys.twilioKey && !apiKeys.twilioKey.startsWith('***')) {
|
||||
currentSettings.apiKeys.twilioKey = apiKeys.twilioKey;
|
||||
}
|
||||
if (apiKeys.googleClientId && !apiKeys.googleClientId.startsWith('***')) {
|
||||
currentSettings.apiKeys.googleClientId = apiKeys.googleClientId;
|
||||
// Update the environment variable for Google OAuth
|
||||
process.env.GOOGLE_CLIENT_ID = apiKeys.googleClientId;
|
||||
}
|
||||
if (apiKeys.googleClientSecret && !apiKeys.googleClientSecret.startsWith('***')) {
|
||||
currentSettings.apiKeys.googleClientSecret = apiKeys.googleClientSecret;
|
||||
// Update the environment variable for Google OAuth
|
||||
process.env.GOOGLE_CLIENT_SECRET = apiKeys.googleClientSecret;
|
||||
}
|
||||
}
|
||||
|
||||
// Update system settings
|
||||
if (systemSettings) {
|
||||
currentSettings.systemSettings = { ...currentSettings.systemSettings, ...systemSettings };
|
||||
}
|
||||
|
||||
// Save the updated settings
|
||||
await enhancedDataService.updateAdminSettings(currentSettings);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update admin settings' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/admin/test-api/:apiType', async (req: Request, res: Response) => {
|
||||
const adminAuth = req.headers['admin-auth'];
|
||||
|
||||
if (adminAuth !== 'true') {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const { apiType } = req.params;
|
||||
const { apiKey } = req.body;
|
||||
|
||||
try {
|
||||
switch (apiType) {
|
||||
case 'aviationStackKey':
|
||||
// Test AviationStack API
|
||||
const testUrl = `http://api.aviationstack.com/v1/flights?access_key=${apiKey}&limit=1`;
|
||||
const response = await fetch(testUrl);
|
||||
|
||||
if (response.ok) {
|
||||
const data: any = await response.json();
|
||||
if (data.error) {
|
||||
res.status(400).json({ error: data.error.message || 'Invalid API key' });
|
||||
} else {
|
||||
res.json({ success: true, message: 'API key is valid!' });
|
||||
}
|
||||
} else {
|
||||
res.status(400).json({ error: 'Failed to validate API key' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'googleMapsKey':
|
||||
res.json({ success: true, message: 'Google Maps API testing not yet implemented' });
|
||||
break;
|
||||
|
||||
case 'twilioKey':
|
||||
res.json({ success: true, message: 'Twilio API testing not yet implemented' });
|
||||
break;
|
||||
|
||||
default:
|
||||
res.status(400).json({ error: 'Unknown API type' });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to test API connection' });
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize database and start server
|
||||
async function startServer() {
|
||||
try {
|
||||
// Initialize database schema and migrate data
|
||||
await databaseService.initializeDatabase();
|
||||
console.log('✅ Database initialization completed');
|
||||
|
||||
// Start the server
|
||||
app.listen(port, () => {
|
||||
console.log(`🚀 Server is running on port ${port}`);
|
||||
console.log(`🔐 Admin password: ${ADMIN_PASSWORD}`);
|
||||
console.log(`📊 Admin dashboard: http://localhost:${port === 3000 ? 5173 : port}/admin`);
|
||||
console.log(`🏥 Health check: http://localhost:${port}/api/health`);
|
||||
console.log(`📚 API docs: http://localhost:${port}/api-docs.html`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startServer();
|
||||
46
backend/src/main.ts
Normal file
46
backend/src/main.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
import { AllExceptionsFilter, HttpExceptionFilter } from './common/filters';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('Bootstrap');
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// Global prefix for all routes
|
||||
app.setGlobalPrefix('api/v1');
|
||||
|
||||
// Enable CORS
|
||||
app.enableCors({
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// Global exception filters (order matters - most specific last)
|
||||
app.useGlobalFilters(
|
||||
new AllExceptionsFilter(),
|
||||
new HttpExceptionFilter(),
|
||||
);
|
||||
|
||||
// Global validation pipe
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true, // Strip properties that don't have decorators
|
||||
forbidNonWhitelisted: true, // Throw error if non-whitelisted properties are present
|
||||
transform: true, // Automatically transform payloads to DTO instances
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
await app.listen(port);
|
||||
|
||||
logger.log(`🚀 Application is running on: http://localhost:${port}/api/v1`);
|
||||
logger.log(`📚 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
logger.log(`🔐 Auth0 Domain: ${process.env.AUTH0_DOMAIN || 'not configured'}`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
9
backend/src/prisma/prisma.module.ts
Normal file
9
backend/src/prisma/prisma.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Global() // Makes PrismaService available everywhere without importing
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
51
backend/src/prisma/prisma.service.ts
Normal file
51
backend/src/prisma/prisma.service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(PrismaService.name);
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
try {
|
||||
await this.$connect();
|
||||
this.logger.log('✅ Database connected successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('❌ Database connection failed', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
this.logger.log('Database disconnected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean database method for testing
|
||||
* WARNING: Only use in development/testing!
|
||||
*/
|
||||
async cleanDatabase() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new Error('Cannot clean database in production!');
|
||||
}
|
||||
|
||||
const models = Object.keys(this).filter(
|
||||
(key) => !key.startsWith('_') && !key.startsWith('$'),
|
||||
);
|
||||
|
||||
return Promise.all(
|
||||
models.map((modelKey) => {
|
||||
const model = this[modelKey as keyof this];
|
||||
if (model && typeof model === 'object' && 'deleteMany' in model) {
|
||||
return (model as any).deleteMany();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import {
|
||||
generateToken,
|
||||
verifyToken,
|
||||
getGoogleAuthUrl,
|
||||
exchangeCodeForTokens,
|
||||
getGoogleUserInfo,
|
||||
User
|
||||
} from '../config/simpleAuth';
|
||||
import databaseService from '../services/databaseService';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Middleware to check authentication
|
||||
export function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const user = verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
|
||||
(req as any).user = user;
|
||||
next();
|
||||
}
|
||||
|
||||
// Middleware to check role
|
||||
export function requireRole(roles: string[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const user = (req as any).user;
|
||||
|
||||
if (!user || !roles.includes(user.role)) {
|
||||
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Get current user
|
||||
router.get('/me', requireAuth, (req: Request, res: Response) => {
|
||||
res.json((req as any).user);
|
||||
});
|
||||
|
||||
// Setup status endpoint (required by frontend)
|
||||
router.get('/setup', async (req: Request, res: Response) => {
|
||||
const clientId = process.env.GOOGLE_CLIENT_ID;
|
||||
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
||||
|
||||
try {
|
||||
const userCount = await databaseService.getUserCount();
|
||||
res.json({
|
||||
setupCompleted: !!(clientId && clientSecret && clientId !== 'your-google-client-id-from-console'),
|
||||
firstAdminCreated: userCount > 0,
|
||||
oauthConfigured: !!(clientId && clientSecret)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking setup status:', error);
|
||||
res.status(500).json({ error: 'Database connection error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Start Google OAuth flow
|
||||
router.get('/google', (req: Request, res: Response) => {
|
||||
try {
|
||||
const authUrl = 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: Request, res: Response) => {
|
||||
const { code, error } = req.query;
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||
|
||||
if (error) {
|
||||
console.error('OAuth error:', error);
|
||||
return res.redirect(`${frontendUrl}?error=${error}`);
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
return res.redirect(`${frontendUrl}?error=no_code`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Exchange code for tokens
|
||||
const tokens = await exchangeCodeForTokens(code as string);
|
||||
|
||||
// Get user info
|
||||
const googleUser = await getGoogleUserInfo(tokens.access_token);
|
||||
|
||||
// Check if user exists or create new user
|
||||
let user = await databaseService.getUserByEmail(googleUser.email);
|
||||
|
||||
if (!user) {
|
||||
// Determine role - first user becomes admin, others need approval
|
||||
const approvedUserCount = await databaseService.getApprovedUserCount();
|
||||
const role = approvedUserCount === 0 ? 'administrator' : 'coordinator';
|
||||
|
||||
user = await databaseService.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.updateUserApprovalStatus(googleUser.email, 'approved');
|
||||
user.approval_status = 'approved';
|
||||
}
|
||||
} else {
|
||||
// Update last sign in
|
||||
await databaseService.updateUserLastSignIn(googleUser.email);
|
||||
console.log(`✅ User logged in: ${user.name} (${user.email})`);
|
||||
}
|
||||
|
||||
// Check if user is approved
|
||||
if (user.approval_status !== 'approved') {
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||
return res.redirect(`${frontendUrl}?error=pending_approval&message=Your account is pending administrator approval`);
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = generateToken(user);
|
||||
|
||||
// Redirect to frontend with token
|
||||
res.redirect(`${frontendUrl}/auth/callback?token=${token}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in OAuth callback:', error);
|
||||
res.redirect(`${frontendUrl}?error=oauth_failed`);
|
||||
}
|
||||
});
|
||||
|
||||
// Exchange OAuth code for JWT token (alternative endpoint for frontend)
|
||||
router.post('/google/exchange', async (req: Request, res: Response) => {
|
||||
const { code } = req.body;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: 'Authorization code is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Exchange code for tokens
|
||||
const tokens = await exchangeCodeForTokens(code);
|
||||
|
||||
// Get user info
|
||||
const googleUser = await getGoogleUserInfo(tokens.access_token);
|
||||
|
||||
// Check if user exists or create new user
|
||||
let user = await databaseService.getUserByEmail(googleUser.email);
|
||||
|
||||
if (!user) {
|
||||
// Determine role - first user becomes admin
|
||||
const userCount = await databaseService.getUserCount();
|
||||
const role = userCount === 0 ? 'administrator' : 'coordinator';
|
||||
|
||||
user = await databaseService.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.updateUserLastSignIn(googleUser.email);
|
||||
console.log(`✅ User logged in: ${user.name} (${user.email})`);
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = 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: Request, res: Response) => {
|
||||
try {
|
||||
const authUrl = 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: Request, res: Response) => {
|
||||
// With JWT, logout is handled client-side by removing the token
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
});
|
||||
|
||||
// Get auth status
|
||||
router.get('/status', (req: Request, res: Response) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.json({ authenticated: false });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const user = 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: Request, res: Response) => {
|
||||
try {
|
||||
const users = await databaseService.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: Request, res: Response) => {
|
||||
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.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: Request, res: Response) => {
|
||||
const { email } = req.params;
|
||||
const currentUser = (req as any).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.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: Request, res: Response) => {
|
||||
const { email } = req.params;
|
||||
|
||||
try {
|
||||
const user = await databaseService.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: Request, res: Response) => {
|
||||
try {
|
||||
const pendingUsers = await databaseService.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: Request, res: Response) => {
|
||||
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.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' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,306 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
interface DataStore {
|
||||
vips: any[];
|
||||
drivers: any[];
|
||||
schedules: { [vipId: string]: any[] };
|
||||
adminSettings: any;
|
||||
users: any[];
|
||||
}
|
||||
|
||||
class DataService {
|
||||
private dataDir: string;
|
||||
private dataFile: string;
|
||||
private data: DataStore;
|
||||
|
||||
constructor() {
|
||||
this.dataDir = path.join(process.cwd(), 'data');
|
||||
this.dataFile = path.join(this.dataDir, 'vip-coordinator.json');
|
||||
|
||||
// Ensure data directory exists
|
||||
if (!fs.existsSync(this.dataDir)) {
|
||||
fs.mkdirSync(this.dataDir, { recursive: true });
|
||||
}
|
||||
|
||||
this.data = this.loadData();
|
||||
}
|
||||
|
||||
private loadData(): DataStore {
|
||||
try {
|
||||
if (fs.existsSync(this.dataFile)) {
|
||||
const fileContent = fs.readFileSync(this.dataFile, 'utf8');
|
||||
const loadedData = JSON.parse(fileContent);
|
||||
console.log(`✅ Loaded data from ${this.dataFile}`);
|
||||
console.log(` - VIPs: ${loadedData.vips?.length || 0}`);
|
||||
console.log(` - Drivers: ${loadedData.drivers?.length || 0}`);
|
||||
console.log(` - Users: ${loadedData.users?.length || 0}`);
|
||||
console.log(` - Schedules: ${Object.keys(loadedData.schedules || {}).length} VIPs with schedules`);
|
||||
|
||||
// Ensure users array exists for backward compatibility
|
||||
if (!loadedData.users) {
|
||||
loadedData.users = [];
|
||||
}
|
||||
|
||||
return loadedData;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data file:', error);
|
||||
}
|
||||
|
||||
// Return default empty data structure
|
||||
console.log('📝 Starting with empty data store');
|
||||
return {
|
||||
vips: [],
|
||||
drivers: [],
|
||||
schedules: {},
|
||||
users: [],
|
||||
adminSettings: {
|
||||
apiKeys: {
|
||||
aviationStackKey: process.env.AVIATIONSTACK_API_KEY || '',
|
||||
googleMapsKey: '',
|
||||
twilioKey: ''
|
||||
},
|
||||
systemSettings: {
|
||||
defaultPickupLocation: '',
|
||||
defaultDropoffLocation: '',
|
||||
timeZone: 'America/New_York',
|
||||
notificationsEnabled: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private saveData(): void {
|
||||
try {
|
||||
const dataToSave = JSON.stringify(this.data, null, 2);
|
||||
fs.writeFileSync(this.dataFile, dataToSave, 'utf8');
|
||||
console.log(`💾 Data saved to ${this.dataFile}`);
|
||||
} catch (error) {
|
||||
console.error('Error saving data file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// VIP operations
|
||||
getVips(): any[] {
|
||||
return this.data.vips;
|
||||
}
|
||||
|
||||
addVip(vip: any): any {
|
||||
this.data.vips.push(vip);
|
||||
this.saveData();
|
||||
return vip;
|
||||
}
|
||||
|
||||
updateVip(id: string, updatedVip: any): any | null {
|
||||
const index = this.data.vips.findIndex(vip => vip.id === id);
|
||||
if (index !== -1) {
|
||||
this.data.vips[index] = updatedVip;
|
||||
this.saveData();
|
||||
return this.data.vips[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
deleteVip(id: string): any | null {
|
||||
const index = this.data.vips.findIndex(vip => vip.id === id);
|
||||
if (index !== -1) {
|
||||
const deletedVip = this.data.vips.splice(index, 1)[0];
|
||||
// Also delete the VIP's schedule
|
||||
delete this.data.schedules[id];
|
||||
this.saveData();
|
||||
return deletedVip;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Driver operations
|
||||
getDrivers(): any[] {
|
||||
return this.data.drivers;
|
||||
}
|
||||
|
||||
addDriver(driver: any): any {
|
||||
this.data.drivers.push(driver);
|
||||
this.saveData();
|
||||
return driver;
|
||||
}
|
||||
|
||||
updateDriver(id: string, updatedDriver: any): any | null {
|
||||
const index = this.data.drivers.findIndex(driver => driver.id === id);
|
||||
if (index !== -1) {
|
||||
this.data.drivers[index] = updatedDriver;
|
||||
this.saveData();
|
||||
return this.data.drivers[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
deleteDriver(id: string): any | null {
|
||||
const index = this.data.drivers.findIndex(driver => driver.id === id);
|
||||
if (index !== -1) {
|
||||
const deletedDriver = this.data.drivers.splice(index, 1)[0];
|
||||
this.saveData();
|
||||
return deletedDriver;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Schedule operations
|
||||
getSchedule(vipId: string): any[] {
|
||||
return this.data.schedules[vipId] || [];
|
||||
}
|
||||
|
||||
addScheduleEvent(vipId: string, event: any): any {
|
||||
if (!this.data.schedules[vipId]) {
|
||||
this.data.schedules[vipId] = [];
|
||||
}
|
||||
this.data.schedules[vipId].push(event);
|
||||
this.saveData();
|
||||
return event;
|
||||
}
|
||||
|
||||
updateScheduleEvent(vipId: string, eventId: string, updatedEvent: any): any | null {
|
||||
if (!this.data.schedules[vipId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = this.data.schedules[vipId].findIndex(event => event.id === eventId);
|
||||
if (index !== -1) {
|
||||
this.data.schedules[vipId][index] = updatedEvent;
|
||||
this.saveData();
|
||||
return this.data.schedules[vipId][index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
deleteScheduleEvent(vipId: string, eventId: string): any | null {
|
||||
if (!this.data.schedules[vipId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = this.data.schedules[vipId].findIndex(event => event.id === eventId);
|
||||
if (index !== -1) {
|
||||
const deletedEvent = this.data.schedules[vipId].splice(index, 1)[0];
|
||||
this.saveData();
|
||||
return deletedEvent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getAllSchedules(): { [vipId: string]: any[] } {
|
||||
return this.data.schedules;
|
||||
}
|
||||
|
||||
// Admin settings operations
|
||||
getAdminSettings(): any {
|
||||
return this.data.adminSettings;
|
||||
}
|
||||
|
||||
updateAdminSettings(settings: any): void {
|
||||
this.data.adminSettings = { ...this.data.adminSettings, ...settings };
|
||||
this.saveData();
|
||||
}
|
||||
|
||||
// Backup and restore operations
|
||||
createBackup(): string {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupFile = path.join(this.dataDir, `backup-${timestamp}.json`);
|
||||
|
||||
try {
|
||||
fs.copyFileSync(this.dataFile, backupFile);
|
||||
console.log(`📦 Backup created: ${backupFile}`);
|
||||
return backupFile;
|
||||
} catch (error) {
|
||||
console.error('Error creating backup:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// User operations
|
||||
getUsers(): any[] {
|
||||
return this.data.users;
|
||||
}
|
||||
|
||||
getUserByEmail(email: string): any | null {
|
||||
return this.data.users.find(user => user.email === email) || null;
|
||||
}
|
||||
|
||||
getUserById(id: string): any | null {
|
||||
return this.data.users.find(user => user.id === id) || null;
|
||||
}
|
||||
|
||||
addUser(user: any): any {
|
||||
// Add timestamps
|
||||
const userWithTimestamps = {
|
||||
...user,
|
||||
created_at: new Date().toISOString(),
|
||||
last_sign_in_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.data.users.push(userWithTimestamps);
|
||||
this.saveData();
|
||||
console.log(`👤 Added user: ${user.name} (${user.email}) as ${user.role}`);
|
||||
return userWithTimestamps;
|
||||
}
|
||||
|
||||
updateUser(email: string, updatedUser: any): any | null {
|
||||
const index = this.data.users.findIndex(user => user.email === email);
|
||||
if (index !== -1) {
|
||||
this.data.users[index] = { ...this.data.users[index], ...updatedUser };
|
||||
this.saveData();
|
||||
console.log(`👤 Updated user: ${this.data.users[index].name} (${email})`);
|
||||
return this.data.users[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
updateUserRole(email: string, role: string): any | null {
|
||||
const index = this.data.users.findIndex(user => user.email === email);
|
||||
if (index !== -1) {
|
||||
this.data.users[index].role = role;
|
||||
this.saveData();
|
||||
console.log(`👤 Updated user role: ${this.data.users[index].name} (${email}) -> ${role}`);
|
||||
return this.data.users[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
updateUserLastSignIn(email: string): any | null {
|
||||
const index = this.data.users.findIndex(user => user.email === email);
|
||||
if (index !== -1) {
|
||||
this.data.users[index].last_sign_in_at = new Date().toISOString();
|
||||
this.saveData();
|
||||
return this.data.users[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
deleteUser(email: string): any | null {
|
||||
const index = this.data.users.findIndex(user => user.email === email);
|
||||
if (index !== -1) {
|
||||
const deletedUser = this.data.users.splice(index, 1)[0];
|
||||
this.saveData();
|
||||
console.log(`👤 Deleted user: ${deletedUser.name} (${email})`);
|
||||
return deletedUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getUserCount(): number {
|
||||
return this.data.users.length;
|
||||
}
|
||||
|
||||
getDataStats(): any {
|
||||
return {
|
||||
vips: this.data.vips.length,
|
||||
drivers: this.data.drivers.length,
|
||||
users: this.data.users.length,
|
||||
scheduledEvents: Object.values(this.data.schedules).reduce((total, events) => total + events.length, 0),
|
||||
vipsWithSchedules: Object.keys(this.data.schedules).length,
|
||||
dataFile: this.dataFile,
|
||||
lastModified: fs.existsSync(this.dataFile) ? fs.statSync(this.dataFile).mtime : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new DataService();
|
||||
@@ -1,550 +0,0 @@
|
||||
import { Pool, PoolClient } from 'pg';
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
class DatabaseService {
|
||||
private pool: Pool;
|
||||
private redis: RedisClientType;
|
||||
|
||||
constructor() {
|
||||
this.pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||
});
|
||||
|
||||
// Initialize Redis connection
|
||||
this.redis = createClient({
|
||||
socket: {
|
||||
host: process.env.REDIS_HOST || 'redis',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379')
|
||||
}
|
||||
});
|
||||
|
||||
this.redis.on('error', (err) => {
|
||||
console.error('❌ Redis connection error:', err);
|
||||
});
|
||||
|
||||
// Test connections on startup
|
||||
this.testConnection();
|
||||
this.testRedisConnection();
|
||||
}
|
||||
|
||||
private async testConnection(): Promise<void> {
|
||||
try {
|
||||
const client = await this.pool.connect();
|
||||
console.log('✅ Connected to PostgreSQL database');
|
||||
client.release();
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to PostgreSQL database:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async testRedisConnection(): Promise<void> {
|
||||
try {
|
||||
if (!this.redis.isOpen) {
|
||||
await this.redis.connect();
|
||||
}
|
||||
await this.redis.ping();
|
||||
console.log('✅ Connected to Redis');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to Redis:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async query(text: string, params?: any[]): Promise<any> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
const result = await client.query(text, params);
|
||||
return result;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getClient(): Promise<PoolClient> {
|
||||
return await this.pool.connect();
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.pool.end();
|
||||
if (this.redis.isOpen) {
|
||||
await this.redis.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize database tables
|
||||
async initializeTables(): Promise<void> {
|
||||
try {
|
||||
// Create users table (matching the actual schema)
|
||||
await this.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
google_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL CHECK (role IN ('driver', 'coordinator', 'administrator')),
|
||||
profile_picture_url TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
approval_status VARCHAR(20) DEFAULT 'pending' CHECK (approval_status IN ('pending', 'approved', 'denied'))
|
||||
)
|
||||
`);
|
||||
|
||||
// Add approval_status column if it doesn't exist (migration for existing databases)
|
||||
await this.query(`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS approval_status VARCHAR(20) DEFAULT 'pending' CHECK (approval_status IN ('pending', 'approved', 'denied'))
|
||||
`);
|
||||
|
||||
// Create indexes
|
||||
await this.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id)
|
||||
`);
|
||||
|
||||
await this.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)
|
||||
`);
|
||||
|
||||
await this.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role)
|
||||
`);
|
||||
|
||||
console.log('✅ Database tables initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize database tables:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// User management methods
|
||||
async createUser(user: {
|
||||
id: string;
|
||||
google_id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
profile_picture_url?: string;
|
||||
role: string;
|
||||
}): Promise<any> {
|
||||
const query = `
|
||||
INSERT INTO users (id, google_id, email, name, profile_picture_url, role, last_login)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const values = [
|
||||
user.id,
|
||||
user.google_id,
|
||||
user.email,
|
||||
user.name,
|
||||
user.profile_picture_url || null,
|
||||
user.role
|
||||
];
|
||||
|
||||
const result = await this.query(query, values);
|
||||
console.log(`👤 Created user: ${user.name} (${user.email}) as ${user.role}`);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<any> {
|
||||
const query = 'SELECT * FROM users WHERE email = $1';
|
||||
const result = await this.query(query, [email]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async getUserById(id: string): Promise<any> {
|
||||
const query = 'SELECT * FROM users WHERE id = $1';
|
||||
const result = await this.query(query, [id]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async getAllUsers(): Promise<any[]> {
|
||||
const query = 'SELECT * FROM users ORDER BY created_at ASC';
|
||||
const result = await this.query(query);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async updateUserRole(email: string, role: string): Promise<any> {
|
||||
const query = `
|
||||
UPDATE users
|
||||
SET role = $1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE email = $2
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await this.query(query, [role, email]);
|
||||
if (result.rows[0]) {
|
||||
console.log(`👤 Updated user role: ${result.rows[0].name} (${email}) -> ${role}`);
|
||||
}
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async updateUserLastSignIn(email: string): Promise<any> {
|
||||
const query = `
|
||||
UPDATE users
|
||||
SET last_login = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE email = $1
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await this.query(query, [email]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async deleteUser(email: string): Promise<any> {
|
||||
const query = 'DELETE FROM users WHERE email = $1 RETURNING *';
|
||||
const result = await this.query(query, [email]);
|
||||
if (result.rows[0]) {
|
||||
console.log(`👤 Deleted user: ${result.rows[0].name} (${email})`);
|
||||
}
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async getUserCount(): Promise<number> {
|
||||
const query = 'SELECT COUNT(*) as count FROM users';
|
||||
const result = await this.query(query);
|
||||
return parseInt(result.rows[0].count);
|
||||
}
|
||||
|
||||
// User approval methods
|
||||
async updateUserApprovalStatus(email: string, status: 'pending' | 'approved' | 'denied'): Promise<any> {
|
||||
const query = `
|
||||
UPDATE users
|
||||
SET approval_status = $1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE email = $2
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await this.query(query, [status, email]);
|
||||
if (result.rows[0]) {
|
||||
console.log(`👤 Updated user approval: ${result.rows[0].name} (${email}) -> ${status}`);
|
||||
}
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async getPendingUsers(): Promise<any[]> {
|
||||
const query = 'SELECT * FROM users WHERE approval_status = $1 ORDER BY created_at ASC';
|
||||
const result = await this.query(query, ['pending']);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async getApprovedUserCount(): Promise<number> {
|
||||
const query = 'SELECT COUNT(*) as count FROM users WHERE approval_status = $1';
|
||||
const result = await this.query(query, ['approved']);
|
||||
return parseInt(result.rows[0].count);
|
||||
}
|
||||
|
||||
// Initialize all database tables and schema
|
||||
async initializeDatabase(): Promise<void> {
|
||||
try {
|
||||
await this.initializeTables();
|
||||
await this.initializeVipTables();
|
||||
|
||||
// Approve all existing users (migration for approval system)
|
||||
await this.query(`
|
||||
UPDATE users
|
||||
SET approval_status = 'approved'
|
||||
WHERE approval_status IS NULL OR approval_status = 'pending'
|
||||
`);
|
||||
console.log('✅ Approved all existing users');
|
||||
|
||||
console.log('✅ Database schema initialization completed');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize database schema:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// VIP table initialization using the correct schema
|
||||
async initializeVipTables(): Promise<void> {
|
||||
try {
|
||||
// Check if VIPs table exists and has the correct schema
|
||||
const tableExists = await this.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'vips'
|
||||
)
|
||||
`);
|
||||
|
||||
if (tableExists.rows[0].exists) {
|
||||
// Check if the table has the correct columns
|
||||
const columnCheck = await this.query(`
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'vips'
|
||||
AND column_name = 'organization'
|
||||
`);
|
||||
|
||||
if (columnCheck.rows.length === 0) {
|
||||
console.log('🔄 Migrating VIPs table to new schema...');
|
||||
// Drop the old table and recreate with correct schema
|
||||
await this.query(`DROP TABLE IF EXISTS vips CASCADE`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create VIPs table with correct schema matching enhancedDataService expectations
|
||||
await this.query(`
|
||||
CREATE TABLE IF NOT EXISTS vips (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
organization VARCHAR(255) NOT NULL,
|
||||
department VARCHAR(255) DEFAULT 'Office of Development',
|
||||
transport_mode VARCHAR(50) NOT NULL CHECK (transport_mode IN ('flight', 'self-driving')),
|
||||
expected_arrival TIMESTAMP,
|
||||
needs_airport_pickup BOOLEAN DEFAULT false,
|
||||
needs_venue_transport BOOLEAN DEFAULT true,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Create flights table (for VIPs with flight transport)
|
||||
await this.query(`
|
||||
CREATE TABLE IF NOT EXISTS flights (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
|
||||
flight_number VARCHAR(50) NOT NULL,
|
||||
flight_date DATE NOT NULL,
|
||||
segment INTEGER NOT NULL,
|
||||
departure_airport VARCHAR(10),
|
||||
arrival_airport VARCHAR(10),
|
||||
scheduled_departure TIMESTAMP,
|
||||
scheduled_arrival TIMESTAMP,
|
||||
actual_departure TIMESTAMP,
|
||||
actual_arrival TIMESTAMP,
|
||||
status VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Check and migrate drivers table
|
||||
const driversTableExists = await this.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'drivers'
|
||||
)
|
||||
`);
|
||||
|
||||
if (driversTableExists.rows[0].exists) {
|
||||
// Check if drivers table has the correct schema (phone column and department column)
|
||||
const driversSchemaCheck = await this.query(`
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'drivers'
|
||||
AND column_name IN ('phone', 'department')
|
||||
`);
|
||||
|
||||
if (driversSchemaCheck.rows.length < 2) {
|
||||
console.log('🔄 Migrating drivers table to new schema...');
|
||||
await this.query(`DROP TABLE IF EXISTS drivers CASCADE`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create drivers table with correct schema
|
||||
await this.query(`
|
||||
CREATE TABLE IF NOT EXISTS drivers (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(50) NOT NULL,
|
||||
department VARCHAR(255) DEFAULT 'Office of Development',
|
||||
user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Check and migrate schedule_events table
|
||||
const scheduleTableExists = await this.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'schedule_events'
|
||||
)
|
||||
`);
|
||||
|
||||
if (!scheduleTableExists.rows[0].exists) {
|
||||
// Check for old 'schedules' table and drop it
|
||||
const oldScheduleExists = await this.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'schedules'
|
||||
)
|
||||
`);
|
||||
|
||||
if (oldScheduleExists.rows[0].exists) {
|
||||
console.log('🔄 Migrating schedules table to schedule_events...');
|
||||
await this.query(`DROP TABLE IF EXISTS schedules CASCADE`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create schedule_events table
|
||||
await this.query(`
|
||||
CREATE TABLE IF NOT EXISTS schedule_events (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
vip_id VARCHAR(255) REFERENCES vips(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
location VARCHAR(255) NOT NULL,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
description TEXT,
|
||||
assigned_driver_id VARCHAR(255) REFERENCES drivers(id) ON DELETE SET NULL,
|
||||
status VARCHAR(50) DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'in-progress', 'completed', 'cancelled')),
|
||||
event_type VARCHAR(50) NOT NULL CHECK (event_type IN ('transport', 'meeting', 'event', 'meal', 'accommodation')),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Create system_setup table for tracking initial setup
|
||||
await this.query(`
|
||||
CREATE TABLE IF NOT EXISTS system_setup (
|
||||
id SERIAL PRIMARY KEY,
|
||||
setup_completed BOOLEAN DEFAULT false,
|
||||
first_admin_created BOOLEAN DEFAULT false,
|
||||
setup_date TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Create admin_settings table
|
||||
await this.query(`
|
||||
CREATE TABLE IF NOT EXISTS admin_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
setting_key VARCHAR(255) UNIQUE NOT NULL,
|
||||
setting_value TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indexes for better performance
|
||||
await this.query(`CREATE INDEX IF NOT EXISTS idx_vips_transport_mode ON vips(transport_mode)`);
|
||||
await this.query(`CREATE INDEX IF NOT EXISTS idx_flights_vip_id ON flights(vip_id)`);
|
||||
await this.query(`CREATE INDEX IF NOT EXISTS idx_flights_date ON flights(flight_date)`);
|
||||
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_vip_id ON schedule_events(vip_id)`);
|
||||
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_driver_id ON schedule_events(assigned_driver_id)`);
|
||||
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_start_time ON schedule_events(start_time)`);
|
||||
await this.query(`CREATE INDEX IF NOT EXISTS idx_schedule_events_status ON schedule_events(status)`);
|
||||
await this.query(`CREATE INDEX IF NOT EXISTS idx_drivers_user_id ON drivers(user_id)`);
|
||||
|
||||
// Create updated_at trigger function
|
||||
await this.query(`
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql'
|
||||
`);
|
||||
|
||||
// Create triggers for updated_at (drop if exists first)
|
||||
await this.query(`DROP TRIGGER IF EXISTS update_vips_updated_at ON vips`);
|
||||
await this.query(`DROP TRIGGER IF EXISTS update_flights_updated_at ON flights`);
|
||||
await this.query(`DROP TRIGGER IF EXISTS update_drivers_updated_at ON drivers`);
|
||||
await this.query(`DROP TRIGGER IF EXISTS update_schedule_events_updated_at ON schedule_events`);
|
||||
await this.query(`DROP TRIGGER IF EXISTS update_admin_settings_updated_at ON admin_settings`);
|
||||
|
||||
await this.query(`CREATE TRIGGER update_vips_updated_at BEFORE UPDATE ON vips FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
|
||||
await this.query(`CREATE TRIGGER update_flights_updated_at BEFORE UPDATE ON flights FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
|
||||
await this.query(`CREATE TRIGGER update_drivers_updated_at BEFORE UPDATE ON drivers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
|
||||
await this.query(`CREATE TRIGGER update_schedule_events_updated_at BEFORE UPDATE ON schedule_events FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
|
||||
await this.query(`CREATE TRIGGER update_admin_settings_updated_at BEFORE UPDATE ON admin_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()`);
|
||||
|
||||
console.log('✅ VIP Coordinator database schema initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize VIP tables:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Redis-based driver location tracking
|
||||
async getDriverLocation(driverId: string): Promise<{ lat: number; lng: number } | null> {
|
||||
try {
|
||||
if (!this.redis.isOpen) {
|
||||
await this.redis.connect();
|
||||
}
|
||||
|
||||
const location = await this.redis.hGetAll(`driver:${driverId}:location`);
|
||||
|
||||
if (location && location.lat && location.lng) {
|
||||
return {
|
||||
lat: parseFloat(location.lat),
|
||||
lng: parseFloat(location.lng)
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting driver location from Redis:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async updateDriverLocation(driverId: string, location: { lat: number; lng: number }): Promise<void> {
|
||||
try {
|
||||
if (!this.redis.isOpen) {
|
||||
await this.redis.connect();
|
||||
}
|
||||
|
||||
const key = `driver:${driverId}:location`;
|
||||
await this.redis.hSet(key, {
|
||||
lat: location.lat.toString(),
|
||||
lng: location.lng.toString(),
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Set expiration to 24 hours
|
||||
await this.redis.expire(key, 24 * 60 * 60);
|
||||
} catch (error) {
|
||||
console.error('❌ Error updating driver location in Redis:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllDriverLocations(): Promise<{ [driverId: string]: { lat: number; lng: number } }> {
|
||||
try {
|
||||
if (!this.redis.isOpen) {
|
||||
await this.redis.connect();
|
||||
}
|
||||
|
||||
const keys = await this.redis.keys('driver:*:location');
|
||||
const locations: { [driverId: string]: { lat: number; lng: number } } = {};
|
||||
|
||||
for (const key of keys) {
|
||||
const driverId = key.split(':')[1];
|
||||
const location = await this.redis.hGetAll(key);
|
||||
|
||||
if (location && location.lat && location.lng) {
|
||||
locations[driverId] = {
|
||||
lat: parseFloat(location.lat),
|
||||
lng: parseFloat(location.lng)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return locations;
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting all driver locations from Redis:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async removeDriverLocation(driverId: string): Promise<void> {
|
||||
try {
|
||||
if (!this.redis.isOpen) {
|
||||
await this.redis.connect();
|
||||
}
|
||||
|
||||
await this.redis.del(`driver:${driverId}:location`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error removing driver location from Redis:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new DatabaseService();
|
||||
@@ -1,184 +0,0 @@
|
||||
interface ScheduleEvent {
|
||||
id: string;
|
||||
title: string;
|
||||
location: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
assignedDriverId?: string;
|
||||
vipId: string;
|
||||
vipName: string;
|
||||
}
|
||||
|
||||
interface ConflictInfo {
|
||||
type: 'overlap' | 'tight_turnaround' | 'back_to_back';
|
||||
severity: 'low' | 'medium' | 'high';
|
||||
message: string;
|
||||
conflictingEvent: ScheduleEvent;
|
||||
timeDifference?: number; // minutes
|
||||
}
|
||||
|
||||
interface DriverAvailability {
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
status: 'available' | 'scheduled' | 'overlapping' | 'tight_turnaround';
|
||||
assignmentCount: number;
|
||||
conflicts: ConflictInfo[];
|
||||
currentAssignments: ScheduleEvent[];
|
||||
}
|
||||
|
||||
class DriverConflictService {
|
||||
|
||||
// Check for conflicts when assigning a driver to an event
|
||||
checkDriverConflicts(
|
||||
driverId: string,
|
||||
newEvent: { startTime: string; endTime: string; location: string },
|
||||
allSchedules: { [vipId: string]: ScheduleEvent[] },
|
||||
drivers: any[]
|
||||
): ConflictInfo[] {
|
||||
const conflicts: ConflictInfo[] = [];
|
||||
const driver = drivers.find(d => d.id === driverId);
|
||||
if (!driver) return conflicts;
|
||||
|
||||
// Get all events assigned to this driver
|
||||
const driverEvents = this.getDriverEvents(driverId, allSchedules);
|
||||
|
||||
const newStartTime = new Date(newEvent.startTime);
|
||||
const newEndTime = new Date(newEvent.endTime);
|
||||
|
||||
for (const existingEvent of driverEvents) {
|
||||
const existingStart = new Date(existingEvent.startTime);
|
||||
const existingEnd = new Date(existingEvent.endTime);
|
||||
|
||||
// Check for direct time overlap
|
||||
if (this.hasTimeOverlap(newStartTime, newEndTime, existingStart, existingEnd)) {
|
||||
conflicts.push({
|
||||
type: 'overlap',
|
||||
severity: 'high',
|
||||
message: `Direct time conflict with "${existingEvent.title}" for ${existingEvent.vipName}`,
|
||||
conflictingEvent: existingEvent
|
||||
});
|
||||
}
|
||||
// Check for tight turnaround (less than 15 minutes between events)
|
||||
else {
|
||||
const timeBetween = this.getTimeBetweenEvents(
|
||||
newStartTime, newEndTime, existingStart, existingEnd
|
||||
);
|
||||
|
||||
if (timeBetween !== null && timeBetween < 15) {
|
||||
conflicts.push({
|
||||
type: 'tight_turnaround',
|
||||
severity: timeBetween < 5 ? 'high' : 'medium',
|
||||
message: `Only ${timeBetween} minutes between events. Previous: "${existingEvent.title}"`,
|
||||
conflictingEvent: existingEvent,
|
||||
timeDifference: timeBetween
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
// Get availability status for all drivers for a specific time slot
|
||||
getDriverAvailability(
|
||||
eventTime: { startTime: string; endTime: string; location: string },
|
||||
allSchedules: { [vipId: string]: ScheduleEvent[] },
|
||||
drivers: any[]
|
||||
): DriverAvailability[] {
|
||||
return drivers.map(driver => {
|
||||
const conflicts = this.checkDriverConflicts(driver.id, eventTime, allSchedules, drivers);
|
||||
const driverEvents = this.getDriverEvents(driver.id, allSchedules);
|
||||
|
||||
let status: DriverAvailability['status'] = 'available';
|
||||
|
||||
if (conflicts.length > 0) {
|
||||
const hasOverlap = conflicts.some(c => c.type === 'overlap');
|
||||
const hasTightTurnaround = conflicts.some(c => c.type === 'tight_turnaround');
|
||||
|
||||
if (hasOverlap) {
|
||||
status = 'overlapping';
|
||||
} else if (hasTightTurnaround) {
|
||||
status = 'tight_turnaround';
|
||||
}
|
||||
} else if (driverEvents.length > 0) {
|
||||
status = 'scheduled';
|
||||
}
|
||||
|
||||
return {
|
||||
driverId: driver.id,
|
||||
driverName: driver.name,
|
||||
status,
|
||||
assignmentCount: driverEvents.length,
|
||||
conflicts,
|
||||
currentAssignments: driverEvents
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Get all events assigned to a specific driver
|
||||
private getDriverEvents(driverId: string, allSchedules: { [vipId: string]: ScheduleEvent[] }): ScheduleEvent[] {
|
||||
const driverEvents: ScheduleEvent[] = [];
|
||||
|
||||
Object.entries(allSchedules).forEach(([vipId, events]) => {
|
||||
events.forEach(event => {
|
||||
if (event.assignedDriverId === driverId) {
|
||||
driverEvents.push({
|
||||
...event,
|
||||
vipId,
|
||||
vipName: event.title // We'll need to get actual VIP name from VIP data
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by start time
|
||||
return driverEvents.sort((a, b) =>
|
||||
new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
// Check if two time periods overlap
|
||||
private hasTimeOverlap(
|
||||
start1: Date, end1: Date,
|
||||
start2: Date, end2: Date
|
||||
): boolean {
|
||||
return start1 < end2 && start2 < end1;
|
||||
}
|
||||
|
||||
// Get minutes between two events (null if they overlap)
|
||||
private getTimeBetweenEvents(
|
||||
newStart: Date, newEnd: Date,
|
||||
existingStart: Date, existingEnd: Date
|
||||
): number | null {
|
||||
// If new event is after existing event
|
||||
if (newStart >= existingEnd) {
|
||||
return Math.floor((newStart.getTime() - existingEnd.getTime()) / (1000 * 60));
|
||||
}
|
||||
// If new event is before existing event
|
||||
else if (newEnd <= existingStart) {
|
||||
return Math.floor((existingStart.getTime() - newEnd.getTime()) / (1000 * 60));
|
||||
}
|
||||
// Events overlap
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate summary message for driver status
|
||||
getDriverStatusSummary(availability: DriverAvailability): string {
|
||||
switch (availability.status) {
|
||||
case 'available':
|
||||
return `✅ Fully available (${availability.assignmentCount} assignments)`;
|
||||
case 'scheduled':
|
||||
return `🟡 Has ${availability.assignmentCount} assignment(s) but available for this time`;
|
||||
case 'tight_turnaround':
|
||||
const tightConflict = availability.conflicts.find(c => c.type === 'tight_turnaround');
|
||||
return `⚡ Tight turnaround - ${tightConflict?.timeDifference} min between events`;
|
||||
case 'overlapping':
|
||||
return `🔴 Time conflict with existing assignment`;
|
||||
default:
|
||||
return 'Unknown status';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new DriverConflictService();
|
||||
export { DriverAvailability, ConflictInfo, ScheduleEvent };
|
||||
@@ -1,677 +0,0 @@
|
||||
import pool from '../config/database';
|
||||
import databaseService from './databaseService';
|
||||
|
||||
interface VipData {
|
||||
id: string;
|
||||
name: string;
|
||||
organization: string;
|
||||
department?: string;
|
||||
transportMode: 'flight' | 'self-driving';
|
||||
expectedArrival?: string;
|
||||
needsAirportPickup?: boolean;
|
||||
needsVenueTransport: boolean;
|
||||
notes?: string;
|
||||
flights?: Array<{
|
||||
flightNumber: string;
|
||||
flightDate: string;
|
||||
segment: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface DriverData {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
department?: string;
|
||||
currentLocation?: { lat: number; lng: number };
|
||||
assignedVipIds?: string[];
|
||||
}
|
||||
|
||||
interface ScheduleEventData {
|
||||
id: string;
|
||||
title: string;
|
||||
location: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
description?: string;
|
||||
assignedDriverId?: string;
|
||||
status: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
class EnhancedDataService {
|
||||
|
||||
// VIP operations
|
||||
async getVips(): Promise<VipData[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT v.*,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'flightNumber', f.flight_number,
|
||||
'flightDate', f.flight_date,
|
||||
'segment', f.segment
|
||||
) ORDER BY f.segment
|
||||
) FILTER (WHERE f.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) as flights
|
||||
FROM vips v
|
||||
LEFT JOIN flights f ON v.id = f.vip_id
|
||||
GROUP BY v.id
|
||||
ORDER BY v.name
|
||||
`;
|
||||
|
||||
const result = await pool.query(query);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
organization: row.organization,
|
||||
department: row.department,
|
||||
transportMode: row.transport_mode,
|
||||
expectedArrival: row.expected_arrival,
|
||||
needsAirportPickup: row.needs_airport_pickup,
|
||||
needsVenueTransport: row.needs_venue_transport,
|
||||
notes: row.notes,
|
||||
flights: row.flights
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching VIPs:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async addVip(vip: VipData): Promise<VipData> {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Insert VIP
|
||||
const vipQuery = `
|
||||
INSERT INTO vips (id, name, organization, department, transport_mode, expected_arrival, needs_airport_pickup, needs_venue_transport, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const vipResult = await client.query(vipQuery, [
|
||||
vip.id,
|
||||
vip.name,
|
||||
vip.organization,
|
||||
vip.department || 'Office of Development',
|
||||
vip.transportMode,
|
||||
vip.expectedArrival || null,
|
||||
vip.needsAirportPickup || false,
|
||||
vip.needsVenueTransport,
|
||||
vip.notes || ''
|
||||
]);
|
||||
|
||||
// Insert flights if any
|
||||
if (vip.flights && vip.flights.length > 0) {
|
||||
for (const flight of vip.flights) {
|
||||
const flightQuery = `
|
||||
INSERT INTO flights (vip_id, flight_number, flight_date, segment)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`;
|
||||
|
||||
await client.query(flightQuery, [
|
||||
vip.id,
|
||||
flight.flightNumber,
|
||||
flight.flightDate,
|
||||
flight.segment
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
const savedVip = {
|
||||
...vip,
|
||||
department: vipResult.rows[0].department,
|
||||
transportMode: vipResult.rows[0].transport_mode,
|
||||
expectedArrival: vipResult.rows[0].expected_arrival,
|
||||
needsAirportPickup: vipResult.rows[0].needs_airport_pickup,
|
||||
needsVenueTransport: vipResult.rows[0].needs_venue_transport
|
||||
};
|
||||
|
||||
return savedVip;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('❌ Error adding VIP:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async updateVip(id: string, vip: Partial<VipData>): Promise<VipData | null> {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Update VIP
|
||||
const vipQuery = `
|
||||
UPDATE vips
|
||||
SET name = $2, organization = $3, department = $4, transport_mode = $5,
|
||||
expected_arrival = $6, needs_airport_pickup = $7, needs_venue_transport = $8, notes = $9
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const vipResult = await client.query(vipQuery, [
|
||||
id,
|
||||
vip.name,
|
||||
vip.organization,
|
||||
vip.department || 'Office of Development',
|
||||
vip.transportMode,
|
||||
vip.expectedArrival || null,
|
||||
vip.needsAirportPickup || false,
|
||||
vip.needsVenueTransport,
|
||||
vip.notes || ''
|
||||
]);
|
||||
|
||||
if (vipResult.rows.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Delete existing flights and insert new ones
|
||||
await client.query('DELETE FROM flights WHERE vip_id = $1', [id]);
|
||||
|
||||
if (vip.flights && vip.flights.length > 0) {
|
||||
for (const flight of vip.flights) {
|
||||
const flightQuery = `
|
||||
INSERT INTO flights (vip_id, flight_number, flight_date, segment)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`;
|
||||
|
||||
await client.query(flightQuery, [
|
||||
id,
|
||||
flight.flightNumber,
|
||||
flight.flightDate,
|
||||
flight.segment
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
const updatedVip = {
|
||||
id: vipResult.rows[0].id,
|
||||
name: vipResult.rows[0].name,
|
||||
organization: vipResult.rows[0].organization,
|
||||
department: vipResult.rows[0].department,
|
||||
transportMode: vipResult.rows[0].transport_mode,
|
||||
expectedArrival: vipResult.rows[0].expected_arrival,
|
||||
needsAirportPickup: vipResult.rows[0].needs_airport_pickup,
|
||||
needsVenueTransport: vipResult.rows[0].needs_venue_transport,
|
||||
notes: vipResult.rows[0].notes,
|
||||
flights: vip.flights || []
|
||||
};
|
||||
|
||||
return updatedVip;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('❌ Error updating VIP:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteVip(id: string): Promise<VipData | null> {
|
||||
try {
|
||||
const query = `
|
||||
DELETE FROM vips WHERE id = $1 RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deletedVip = {
|
||||
id: result.rows[0].id,
|
||||
name: result.rows[0].name,
|
||||
organization: result.rows[0].organization,
|
||||
department: result.rows[0].department,
|
||||
transportMode: result.rows[0].transport_mode,
|
||||
expectedArrival: result.rows[0].expected_arrival,
|
||||
needsAirportPickup: result.rows[0].needs_airport_pickup,
|
||||
needsVenueTransport: result.rows[0].needs_venue_transport,
|
||||
notes: result.rows[0].notes
|
||||
};
|
||||
|
||||
return deletedVip;
|
||||
} catch (error) {
|
||||
console.error('❌ Error deleting VIP:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Driver operations
|
||||
async getDrivers(): Promise<DriverData[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT d.*,
|
||||
COALESCE(
|
||||
json_agg(DISTINCT se.vip_id) FILTER (WHERE se.vip_id IS NOT NULL),
|
||||
'[]'::json
|
||||
) as assigned_vip_ids
|
||||
FROM drivers d
|
||||
LEFT JOIN schedule_events se ON d.id = se.assigned_driver_id
|
||||
GROUP BY d.id
|
||||
ORDER BY d.name
|
||||
`;
|
||||
|
||||
const result = await pool.query(query);
|
||||
|
||||
// Get current locations from Redis
|
||||
const driversWithLocations = await Promise.all(
|
||||
result.rows.map(async (row) => {
|
||||
const location = await databaseService.getDriverLocation(row.id);
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
phone: row.phone,
|
||||
department: row.department,
|
||||
currentLocation: location ? { lat: location.lat, lng: location.lng } : { lat: 0, lng: 0 },
|
||||
assignedVipIds: row.assigned_vip_ids || []
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return driversWithLocations;
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching drivers:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async addDriver(driver: DriverData): Promise<DriverData> {
|
||||
try {
|
||||
const query = `
|
||||
INSERT INTO drivers (id, name, phone, department)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
driver.id,
|
||||
driver.name,
|
||||
driver.phone,
|
||||
driver.department || 'Office of Development'
|
||||
]);
|
||||
|
||||
// Store location in Redis if provided
|
||||
if (driver.currentLocation) {
|
||||
await databaseService.updateDriverLocation(driver.id, driver.currentLocation);
|
||||
}
|
||||
|
||||
const savedDriver = {
|
||||
id: result.rows[0].id,
|
||||
name: result.rows[0].name,
|
||||
phone: result.rows[0].phone,
|
||||
department: result.rows[0].department,
|
||||
currentLocation: driver.currentLocation || { lat: 0, lng: 0 }
|
||||
};
|
||||
|
||||
return savedDriver;
|
||||
} catch (error) {
|
||||
console.error('❌ Error adding driver:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateDriver(id: string, driver: Partial<DriverData>): Promise<DriverData | null> {
|
||||
try {
|
||||
const query = `
|
||||
UPDATE drivers
|
||||
SET name = $2, phone = $3, department = $4
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
id,
|
||||
driver.name,
|
||||
driver.phone,
|
||||
driver.department || 'Office of Development'
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update location in Redis if provided
|
||||
if (driver.currentLocation) {
|
||||
await databaseService.updateDriverLocation(id, driver.currentLocation);
|
||||
}
|
||||
|
||||
const updatedDriver = {
|
||||
id: result.rows[0].id,
|
||||
name: result.rows[0].name,
|
||||
phone: result.rows[0].phone,
|
||||
department: result.rows[0].department,
|
||||
currentLocation: driver.currentLocation || { lat: 0, lng: 0 }
|
||||
};
|
||||
|
||||
return updatedDriver;
|
||||
} catch (error) {
|
||||
console.error('❌ Error updating driver:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDriver(id: string): Promise<DriverData | null> {
|
||||
try {
|
||||
const query = `
|
||||
DELETE FROM drivers WHERE id = $1 RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deletedDriver = {
|
||||
id: result.rows[0].id,
|
||||
name: result.rows[0].name,
|
||||
phone: result.rows[0].phone,
|
||||
department: result.rows[0].department
|
||||
};
|
||||
|
||||
return deletedDriver;
|
||||
} catch (error) {
|
||||
console.error('❌ Error deleting driver:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule operations
|
||||
async getSchedule(vipId: string): Promise<ScheduleEventData[]> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT * FROM schedule_events
|
||||
WHERE vip_id = $1
|
||||
ORDER BY start_time
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [vipId]);
|
||||
|
||||
return result.rows.map(row => ({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
location: row.location,
|
||||
startTime: row.start_time,
|
||||
endTime: row.end_time,
|
||||
description: row.description,
|
||||
assignedDriverId: row.assigned_driver_id,
|
||||
status: row.status,
|
||||
type: row.event_type
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching schedule:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async addScheduleEvent(vipId: string, event: ScheduleEventData): Promise<ScheduleEventData> {
|
||||
try {
|
||||
const query = `
|
||||
INSERT INTO schedule_events (id, vip_id, title, location, start_time, end_time, description, assigned_driver_id, status, event_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
event.id,
|
||||
vipId,
|
||||
event.title,
|
||||
event.location,
|
||||
event.startTime,
|
||||
event.endTime,
|
||||
event.description || '',
|
||||
event.assignedDriverId || null,
|
||||
event.status,
|
||||
event.type
|
||||
]);
|
||||
|
||||
const savedEvent = {
|
||||
id: result.rows[0].id,
|
||||
title: result.rows[0].title,
|
||||
location: result.rows[0].location,
|
||||
startTime: result.rows[0].start_time,
|
||||
endTime: result.rows[0].end_time,
|
||||
description: result.rows[0].description,
|
||||
assignedDriverId: result.rows[0].assigned_driver_id,
|
||||
status: result.rows[0].status,
|
||||
type: result.rows[0].event_type
|
||||
};
|
||||
|
||||
return savedEvent;
|
||||
} catch (error) {
|
||||
console.error('❌ Error adding schedule event:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateScheduleEvent(vipId: string, eventId: string, event: ScheduleEventData): Promise<ScheduleEventData | null> {
|
||||
try {
|
||||
const query = `
|
||||
UPDATE schedule_events
|
||||
SET title = $3, location = $4, start_time = $5, end_time = $6, description = $7, assigned_driver_id = $8, status = $9, event_type = $10
|
||||
WHERE id = $1 AND vip_id = $2
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [
|
||||
eventId,
|
||||
vipId,
|
||||
event.title,
|
||||
event.location,
|
||||
event.startTime,
|
||||
event.endTime,
|
||||
event.description || '',
|
||||
event.assignedDriverId || null,
|
||||
event.status,
|
||||
event.type
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updatedEvent = {
|
||||
id: result.rows[0].id,
|
||||
title: result.rows[0].title,
|
||||
location: result.rows[0].location,
|
||||
startTime: result.rows[0].start_time,
|
||||
endTime: result.rows[0].end_time,
|
||||
description: result.rows[0].description,
|
||||
assignedDriverId: result.rows[0].assigned_driver_id,
|
||||
status: result.rows[0].status,
|
||||
type: result.rows[0].event_type
|
||||
};
|
||||
|
||||
return updatedEvent;
|
||||
} catch (error) {
|
||||
console.error('❌ Error updating schedule event:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteScheduleEvent(vipId: string, eventId: string): Promise<ScheduleEventData | null> {
|
||||
try {
|
||||
const query = `
|
||||
DELETE FROM schedule_events
|
||||
WHERE id = $1 AND vip_id = $2
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [eventId, vipId]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deletedEvent = {
|
||||
id: result.rows[0].id,
|
||||
title: result.rows[0].title,
|
||||
location: result.rows[0].location,
|
||||
startTime: result.rows[0].start_time,
|
||||
endTime: result.rows[0].end_time,
|
||||
description: result.rows[0].description,
|
||||
assignedDriverId: result.rows[0].assigned_driver_id,
|
||||
status: result.rows[0].status,
|
||||
type: result.rows[0].event_type
|
||||
};
|
||||
|
||||
return deletedEvent;
|
||||
} catch (error) {
|
||||
console.error('❌ Error deleting schedule event:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getAllSchedules(): Promise<{ [vipId: string]: ScheduleEventData[] }> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT * FROM schedule_events
|
||||
ORDER BY vip_id, start_time
|
||||
`;
|
||||
|
||||
const result = await pool.query(query);
|
||||
|
||||
const schedules: { [vipId: string]: ScheduleEventData[] } = {};
|
||||
|
||||
for (const row of result.rows) {
|
||||
const vipId = row.vip_id;
|
||||
|
||||
if (!schedules[vipId]) {
|
||||
schedules[vipId] = [];
|
||||
}
|
||||
|
||||
schedules[vipId].push({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
location: row.location,
|
||||
startTime: row.start_time,
|
||||
endTime: row.end_time,
|
||||
description: row.description,
|
||||
assignedDriverId: row.assigned_driver_id,
|
||||
status: row.status,
|
||||
type: row.event_type
|
||||
});
|
||||
}
|
||||
|
||||
return schedules;
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching all schedules:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Admin settings operations
|
||||
async getAdminSettings(): Promise<any> {
|
||||
try {
|
||||
const query = `
|
||||
SELECT setting_key, setting_value FROM admin_settings
|
||||
`;
|
||||
|
||||
const result = await pool.query(query);
|
||||
|
||||
// Default settings structure
|
||||
const defaultSettings = {
|
||||
apiKeys: {
|
||||
aviationStackKey: '',
|
||||
googleMapsKey: '',
|
||||
twilioKey: '',
|
||||
googleClientId: '',
|
||||
googleClientSecret: ''
|
||||
},
|
||||
systemSettings: {
|
||||
defaultPickupLocation: '',
|
||||
defaultDropoffLocation: '',
|
||||
timeZone: 'America/New_York',
|
||||
notificationsEnabled: false
|
||||
}
|
||||
};
|
||||
|
||||
// If no settings exist, return defaults
|
||||
if (result.rows.length === 0) {
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
// Reconstruct nested object from flattened keys
|
||||
const settings: any = { ...defaultSettings };
|
||||
|
||||
for (const row of result.rows) {
|
||||
const keys = row.setting_key.split('.');
|
||||
let current = settings;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (!current[keys[i]]) {
|
||||
current[keys[i]] = {};
|
||||
}
|
||||
current = current[keys[i]];
|
||||
}
|
||||
|
||||
// Parse boolean values
|
||||
let value = row.setting_value;
|
||||
if (value === 'true') value = true;
|
||||
else if (value === 'false') value = false;
|
||||
|
||||
current[keys[keys.length - 1]] = value;
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching admin settings:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateAdminSettings(settings: any): Promise<void> {
|
||||
try {
|
||||
// Flatten settings and update
|
||||
const flattenSettings = (obj: any, prefix = ''): Array<{key: string, value: string}> => {
|
||||
const result: Array<{key: string, value: string}> = [];
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
result.push(...flattenSettings(value, fullKey));
|
||||
} else {
|
||||
result.push({ key: fullKey, value: String(value) });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const flatSettings = flattenSettings(settings);
|
||||
|
||||
for (const setting of flatSettings) {
|
||||
const query = `
|
||||
INSERT INTO admin_settings (setting_key, setting_value)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (setting_key) DO UPDATE SET setting_value = $2
|
||||
`;
|
||||
|
||||
await pool.query(query, [setting.key, setting.value]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error updating admin settings:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new EnhancedDataService();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user