From ae3702c3b1d80921fb8d080c0f65941e0b680fe3 Mon Sep 17 00:00:00 2001 From: kyle Date: Sat, 7 Jun 2025 18:32:00 +0200 Subject: [PATCH] Backup: 2025-06-07 18:32 - Production setup complete [Restore from backup: vip-coordinator-backup-2025-06-07-18-32-production-setup-complete] --- .env.prod | 29 ++ .env.production | 30 ++ DOCUMENTATION_CLEANUP_SUMMARY.md | 173 ++++++++ README.md | 299 +++++++++----- SETUP_GUIDE.md | 314 +++++++++++++++ backend/.env | 7 +- backend/Dockerfile | 9 +- backend/package-lock.json | 203 ++-------- backend/package.json | 7 +- backend/src/config/database.ts | 3 - backend/src/config/simpleAuth.ts | 248 +++++------- backend/src/index.ts | 71 +++- backend/src/routes/simpleAuth.ts | 423 ++++++++++++-------- backend/src/services/databaseService.ts | 236 ++++++----- backend/src/services/enhancedDataService.ts | 8 +- backend/src/services/flightService.ts | 6 +- docker-compose.dev.yml | 2 + docker-compose.prod.yml | 40 +- frontend/Dockerfile | 21 +- frontend/nginx.conf | 103 +++-- frontend/package.json | 34 +- frontend/postcss.config.mjs | 6 + frontend/src/App.css | 206 +++++----- frontend/src/App.tsx | 223 +++-------- frontend/src/components/Login.css | 95 ----- frontend/src/components/Login.tsx | 124 +++++- frontend/src/config/api.ts | 15 +- frontend/src/index.css | 288 +++++++++---- frontend/src/main.tsx | 40 +- frontend/src/pages/AdminDashboard.tsx | 338 ++++++++-------- frontend/tailwind.config.js | 3 +- frontend/vite.config.ts | 10 +- 32 files changed, 2120 insertions(+), 1494 deletions(-) create mode 100644 .env.prod create mode 100644 .env.production create mode 100644 DOCUMENTATION_CLEANUP_SUMMARY.md create mode 100644 SETUP_GUIDE.md create mode 100644 frontend/postcss.config.mjs diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..c9b8649 --- /dev/null +++ b/.env.prod @@ -0,0 +1,29 @@ +# 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 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..3d28ef1 --- /dev/null +++ b/.env.production @@ -0,0 +1,30 @@ +# 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 \ No newline at end of file diff --git a/DOCUMENTATION_CLEANUP_SUMMARY.md b/DOCUMENTATION_CLEANUP_SUMMARY.md new file mode 100644 index 0000000..fdb3ddf --- /dev/null +++ b/DOCUMENTATION_CLEANUP_SUMMARY.md @@ -0,0 +1,173 @@ +# 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. \ No newline at end of file diff --git a/README.md b/README.md index 16b5902..24a90c5 100644 --- a/README.md +++ b/README.md @@ -1,155 +1,234 @@ -# VIP Coordinator Dashboard +# VIP Coordinator -A comprehensive web application for managing VIP logistics, driver assignments, and real-time tracking during events. +A comprehensive web application for managing VIP logistics, driver assignments, and real-time tracking with Google OAuth authentication and role-based access control. -## Features +## โœจ Features -- **VIP Management**: Create, edit, and manage VIP profiles with flight information and schedules -- **Driver Coordination**: Assign and track drivers with real-time location updates -- **Flight Tracking**: Monitor flight status and arrival times -- **Schedule Management**: Organize VIP itineraries and driver assignments -- **Real-time Dashboard**: Overview of all active VIPs and available drivers +### ๐Ÿ” 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 -## Tech Stack +### ๐Ÿ‘ฅ 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 + +### ๐Ÿš— 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 + +### โœˆ๏ธ 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 + +### ๐Ÿ“Š 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 with Express.js -- TypeScript for type safety -- PostgreSQL database -- Redis for caching and real-time updates -- Docker containerization +- **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 with TypeScript -- Vite for fast development -- React Router for navigation -- Leaflet for mapping (planned) -- Responsive design with CSS Grid/Flexbox +- **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 -## Getting Started +## ๐Ÿš€ Quick Start ### Prerequisites - Docker and Docker Compose -- Node.js 18+ (for local development) -- npm or yarn +- Google Cloud Console account (for OAuth setup) -### Quick Start with Docker - -1. Clone the repository and navigate to the project directory: +### 1. Start the Application ```bash +git clone cd vip-coordinator -``` - -2. Start the development environment: -```bash make dev ``` -This will start all services: -- Frontend: http://localhost:5173 -- Backend API: http://localhost:3000 -- PostgreSQL: localhost:5432 -- Redis: localhost:6379 +**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 -### Manual Setup +### 2. Configure Google OAuth +See [SETUP_GUIDE.md](SETUP_GUIDE.md) for detailed OAuth setup instructions. -#### Backend Setup -```bash -cd backend -npm install -npm run dev -``` +### 3. First Login +- Visit http://localhost:5173 +- Click "Continue with Google" +- First user becomes system administrator +- Subsequent users need admin approval -#### Frontend Setup -```bash -cd frontend -npm install -npm run dev -``` +## ๐Ÿ“š API Documentation -## API Endpoints +### 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 -### VIPs -- `GET /api/vips` - List all VIPs -- `POST /api/vips` - Create new VIP -- `GET /api/vips/:id` - Get VIP details -- `PUT /api/vips/:id` - Update VIP -- `DELETE /api/vips/:id` - Delete VIP +### 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 -### Drivers -- `GET /api/drivers` - List all drivers -- `POST /api/drivers` - Create new driver -- `GET /api/drivers/:id` - Get driver details -- `PUT /api/drivers/:id` - Update driver -- `DELETE /api/drivers/:id` - Delete driver - -### Health Check -- `GET /api/health` - Service health status - -## Project Structure - -``` -vip-coordinator/ -โ”œโ”€โ”€ backend/ -โ”‚ โ”œโ”€โ”€ src/ -โ”‚ โ”‚ โ””โ”€โ”€ index.ts # Main server file -โ”‚ โ”œโ”€โ”€ package.json -โ”‚ โ”œโ”€โ”€ tsconfig.json -โ”‚ โ””โ”€โ”€ Dockerfile -โ”œโ”€โ”€ frontend/ -โ”‚ โ”œโ”€โ”€ src/ -โ”‚ โ”‚ โ”œโ”€โ”€ components/ # Reusable components -โ”‚ โ”‚ โ”œโ”€โ”€ pages/ # Page components -โ”‚ โ”‚ โ”œโ”€โ”€ types/ # TypeScript types -โ”‚ โ”‚ โ”œโ”€โ”€ 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 -โ””โ”€โ”€ README.md -``` - -## Development Commands +## ๐Ÿ› ๏ธ Development +### Available Commands ```bash # Start development environment make dev -# Build production images +# View logs +make logs + +# Stop all services +make down + +# Rebuild containers make build -# Deploy to production -make deploy - -# Backend only +# Backend development cd backend && npm run dev -# Frontend only +# Frontend development cd frontend && npm run dev ``` -## Planned Features +### 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 +``` +## ๐Ÿ” User Roles & Permissions + +### Administrator +- Full system access +- User management and approval +- System configuration +- All VIP and driver operations + +### Coordinator +- VIP management (create, edit, delete) +- Driver management +- Schedule management +- Flight tracking + +### Driver +- View assigned schedules +- Update task status +- Access driver dashboard + +## ๐ŸŒ Deployment + +### Development +```bash +make dev +``` + +### Production +```bash +# Build production images +make build + +# Deploy with production configuration +docker-compose -f docker-compose.prod.yml up -d +``` + +### Environment Configuration +See [SETUP_GUIDE.md](SETUP_GUIDE.md) for detailed environment variable configuration. + +## ๐Ÿ“‹ Current 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 + +### ๐Ÿšง Planned Features - [ ] Real-time GPS tracking for drivers -- [ ] Flight API integration for live updates - [ ] Push notifications for schedule changes -- [ ] Google Sheets import/export -- [ ] Mobile-responsive driver app -- [ ] Advanced scheduling with drag-and-drop -- [ ] Reporting and analytics +- [ ] Mobile driver application +- [ ] Advanced reporting and analytics +- [ ] Google Sheets integration - [ ] Multi-tenant support +- [ ] Advanced mapping features -## Contributing +## ๐Ÿค Contributing 1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Test thoroughly -5. Submit a pull request +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 -## License +## ๐Ÿ“„ License -This project is licensed under the MIT License. +This project is licensed under the MIT License - see the LICENSE file for details. + +## ๐Ÿ†˜ Support + +- ๐Ÿ“– **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 + +--- + +**VIP Coordinator** - Streamlining VIP logistics with modern web technology. diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 0000000..4386e3e --- /dev/null +++ b/SETUP_GUIDE.md @@ -0,0 +1,314 @@ +# 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 +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. \ No newline at end of file diff --git a/backend/.env b/backend/.env index beac0ba..cca41a3 100644 --- a/backend/.env +++ b/backend/.env @@ -1,5 +1,5 @@ # Database Configuration -DATABASE_URL=postgresql://postgres:password@db:5432/vip_coordinator +DATABASE_URL=postgresql://postgres:changeme@db:5432/vip_coordinator # Redis Configuration REDIS_URL=redis://redis:6379 @@ -8,7 +8,7 @@ REDIS_URL=redis://redis:6379 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 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 @@ -21,3 +21,6 @@ AVIATIONSTACK_API_KEY=your-aviationstack-api-key # Admin Configuration ADMIN_PASSWORD=admin123 + +# Port Configuration +PORT=3000 diff --git a/backend/Dockerfile b/backend/Dockerfile index 59423aa..85a520f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,5 @@ # Multi-stage build for development and production -FROM node:18-alpine AS base +FROM node:22-alpine AS base WORKDIR /app @@ -15,10 +15,7 @@ CMD ["npm", "run", "dev"] # Production stage FROM base AS production -RUN npm ci +RUN npm install COPY . . -RUN npm run build -RUN npm prune --omit=dev -ENV NODE_ENV=production EXPOSE 3000 -CMD ["npm", "start"] +CMD ["npm", "run", "dev"] diff --git a/backend/package-lock.json b/backend/package-lock.json index 89cc6b6..3553cd2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,7 +13,6 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", - "jwks-rsa": "^3.2.0", "pg": "^8.11.3", "redis": "^4.6.8", "uuid": "^9.0.0" @@ -80,7 +79,6 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "license": "MIT", - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -154,6 +152,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -163,6 +162,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -180,6 +180,7 @@ "version": "4.17.22", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -191,6 +192,7 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -201,12 +203,14 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true }, "node_modules/@types/jsonwebtoken": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*", @@ -216,19 +220,21 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "20.17.57", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", - "peer": true, + "dev": true, "dependencies": { "undici-types": "~6.19.2" } @@ -248,17 +254,20 @@ "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==" + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -268,6 +277,7 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -1016,15 +1026,6 @@ "node": ">=0.12.0" } }, - "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1064,46 +1065,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/jwks-rsa": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", - "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", - "license": "MIT", - "dependencies": { - "@types/express": "^4.17.20", - "@types/jsonwebtoken": "^9.0.4", - "debug": "^4.3.4", - "jose": "^4.15.4", - "limiter": "^1.1.5", - "lru-memoizer": "^2.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/jwks-rsa/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/jwks-rsa/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -1114,17 +1075,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1167,28 +1117,6 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/lru-memoizer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", - "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", - "license": "MIT", - "dependencies": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "6.0.0" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -1384,7 +1312,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.0", "pg-pool": "^3.10.0", @@ -1990,7 +1917,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2002,7 +1928,8 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true }, "node_modules/unpipe": { "version": "1.0.0", @@ -2119,7 +2046,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", - "peer": true, "requires": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -2178,6 +2104,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" @@ -2187,6 +2114,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "requires": { "@types/node": "*" } @@ -2204,6 +2132,7 @@ "version": "4.17.22", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", + "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -2215,6 +2144,7 @@ "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, "requires": { "@types/node": "*", "@types/qs": "*", @@ -2225,12 +2155,14 @@ "@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true }, "@types/jsonwebtoken": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, "requires": { "@types/ms": "*", "@types/node": "*" @@ -2239,18 +2171,20 @@ "@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true }, "@types/node": { "version": "20.17.57", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==", - "peer": true, + "dev": true, "requires": { "undici-types": "~6.19.2" } @@ -2269,17 +2203,20 @@ "@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==" + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true }, "@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true }, "@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "requires": { "@types/mime": "^1", "@types/node": "*" @@ -2289,6 +2226,7 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, "requires": { "@types/http-errors": "*", "@types/node": "*", @@ -2841,11 +2779,6 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" - }, "jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -2880,34 +2813,6 @@ "safe-buffer": "^5.0.1" } }, - "jwks-rsa": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", - "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", - "requires": { - "@types/express": "^4.17.20", - "@types/jsonwebtoken": "^9.0.4", - "debug": "^4.3.4", - "jose": "^4.15.4", - "limiter": "^1.1.5", - "lru-memoizer": "^2.2.0" - }, - "dependencies": { - "debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "requires": { - "ms": "^2.1.3" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, "jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -2917,16 +2822,6 @@ "safe-buffer": "^5.0.1" } }, - "limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" - }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2962,23 +2857,6 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "lru-memoizer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", - "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", - "requires": { - "lodash.clonedeep": "^4.5.0", - "lru-cache": "6.0.0" - } - }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -3113,7 +2991,6 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", - "peer": true, "requires": { "pg-cloudflare": "^1.2.5", "pg-connection-string": "^2.9.0", @@ -3509,13 +3386,13 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "peer": true + "dev": true }, "undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true }, "unpipe": { "version": "1.0.0", diff --git a/backend/package.json b/backend/package.json index b4da8f0..9b970bd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "scripts": { "start": "node dist/index.js", - "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "dev": "npx tsx src/index.ts", "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -22,7 +22,6 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", - "jwks-rsa": "^3.2.0", "pg": "^8.11.3", "redis": "^4.6.8", "uuid": "^9.0.0" @@ -34,7 +33,9 @@ "@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", - "typescript": "^5.1.6" + "tsx": "^4.7.0", + "typescript": "^5.6.0" } } diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts index 8d4c985..57e38e5 100644 --- a/backend/src/config/database.ts +++ b/backend/src/config/database.ts @@ -3,11 +3,8 @@ import dotenv from 'dotenv'; dotenv.config(); -const useSSL = process.env.DATABASE_SSL === 'true'; - const pool = new Pool({ connectionString: process.env.DATABASE_URL || 'postgresql://postgres:changeme@localhost:5432/vip_coordinator', - ssl: useSSL ? { rejectUnauthorized: false } : false, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, diff --git a/backend/src/config/simpleAuth.ts b/backend/src/config/simpleAuth.ts index e15c4f9..618abf1 100644 --- a/backend/src/config/simpleAuth.ts +++ b/backend/src/config/simpleAuth.ts @@ -1,178 +1,134 @@ -import jwt, { JwtHeader, JwtPayload } from 'jsonwebtoken'; -import jwksClient from 'jwks-rsa'; +import jwt from 'jsonwebtoken'; -const auth0Domain = process.env.AUTH0_DOMAIN; -const auth0Audience = process.env.AUTH0_AUDIENCE; +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; -if (!auth0Domain) { - console.warn('โš ๏ธ AUTH0_DOMAIN is not set. Authentication routes will reject requests until configured.'); +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; } -if (!auth0Audience) { - console.warn('โš ๏ธ AUTH0_AUDIENCE is not set. Authentication routes will reject requests until configured.'); +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' } + ); } -const jwks = auth0Domain - ? jwksClient({ - jwksUri: `https://${auth0Domain}/.well-known/jwks.json`, - cache: true, - cacheMaxEntries: 5, - cacheMaxAge: 10 * 60 * 1000 - }) - : null; - -const PROFILE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes -const profileCache = new Map(); -const inflightProfileRequests = new Map>(); - -export interface Auth0UserProfile { - sub: string; - email?: string; - name?: string; - nickname?: string; - picture?: string; - [key: string]: unknown; +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; + } } -export interface VerifiedAccessToken extends JwtPayload { - sub: string; - azp?: string; - scope?: string; +// Simple Google OAuth2 client using fetch +export async function verifyGoogleToken(googleToken: string): Promise { + 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; + } } -async function getSigningKey(header: JwtHeader): Promise { - if (!jwks) { - throw new Error('Auth0 JWKS client not initialised'); +// 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'); } - if (!header.kid) { - throw new Error('Token signing key id (kid) is missing'); - } - - const signingKey = await new Promise((resolve, reject) => { - jwks.getSigningKey(header.kid as string, (err, key) => { - if (err) { - return reject(err); - } - if (!key) { - return reject(new Error('Signing key not found')); - } - resolve(key); - }); + const params = new URLSearchParams({ + client_id: clientId, + redirect_uri: redirectUri, + response_type: 'code', + scope: 'openid email profile', + access_type: 'offline', + prompt: 'consent' }); - const publicKey = - typeof signingKey.getPublicKey === 'function' - ? signingKey.getPublicKey() - : (signingKey as any).publicKey || (signingKey as any).rsaPublicKey; - - if (!publicKey) { - throw new Error('Unable to derive signing key'); - } - - return publicKey; + return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`; } -export async function verifyAccessToken(token: string): Promise { - if (!auth0Domain || !auth0Audience) { - throw new Error('Auth0 configuration is incomplete'); +// Exchange authorization code for tokens +export async function exchangeCodeForTokens(code: string): Promise { + 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'); } - const decoded = jwt.decode(token, { complete: true }); - if (!decoded || typeof decoded === 'string') { - throw new Error('Invalid JWT'); - } - - const signingKey = await getSigningKey(decoded.header); - - return jwt.verify(token, signingKey, { - algorithms: ['RS256'], - audience: auth0Audience, - issuer: `https://${auth0Domain}/` - }) as VerifiedAccessToken; -} - -export async function fetchAuth0UserProfile(accessToken: string, cacheKey: string, expiresAt?: number): Promise { - if (!auth0Domain) { - throw new Error('Auth0 configuration is incomplete'); - } - - const now = Date.now(); - const cached = profileCache.get(cacheKey); - if (cached && cached.expiresAt > now) { - return cached.profile; - } - - const ttl = expiresAt ? Math.max(0, expiresAt * 1000 - now) : PROFILE_CACHE_TTL_MS; - if (inflightProfileRequests.has(cacheKey)) { - return inflightProfileRequests.get(cacheKey)!; - } - - const fetchPromise = (async () => { - const response = await fetch(`https://${auth0Domain}/userinfo`, { + try { + const response = await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', headers: { - Authorization: `Bearer ${accessToken}` - } + '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 fetch Auth0 user profile (${response.status})`); + throw new Error('Failed to exchange code for tokens'); } - const profile = (await response.json()) as Auth0UserProfile; - profileCache.set(cacheKey, { profile, expiresAt: now + ttl }); - inflightProfileRequests.delete(cacheKey); - return profile; - })().catch(error => { - inflightProfileRequests.delete(cacheKey); + return await response.json(); + } catch (error) { + console.error('Error exchanging code for tokens:', error); throw error; - }); - - inflightProfileRequests.set(cacheKey, fetchPromise); - return fetchPromise; -} - -export function clearAuth0ProfileCache(cacheKey?: string) { - if (cacheKey) { - profileCache.delete(cacheKey); - inflightProfileRequests.delete(cacheKey); - } else { - profileCache.clear(); - inflightProfileRequests.clear(); } } -export function getCachedProfile(cacheKey: string): Auth0UserProfile | undefined { - const cached = profileCache.get(cacheKey); - if (cached && cached.expiresAt > Date.now()) { - return cached.profile; - } - return undefined; -} - -export function cacheAuth0Profile(cacheKey: string, profile: Auth0UserProfile, expiresAt?: number) { - const ttl = expiresAt ? Math.max(0, expiresAt * 1000 - Date.now()) : PROFILE_CACHE_TTL_MS; - profileCache.set(cacheKey, { profile, expiresAt: Date.now() + ttl }); -} - -export async function fetchFreshAuth0Profile(accessToken: string): Promise { - if (!auth0Domain) { - throw new Error('Auth0 configuration is incomplete'); - } - - const response = await fetch(`https://${auth0Domain}/userinfo`, { - headers: { - Authorization: `Bearer ${accessToken}` +// Get user info from Google +export async function getGoogleUserInfo(accessToken: string): Promise { + 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'); } - }); - if (!response.ok) { - throw new Error(`Failed to fetch Auth0 user profile (${response.status})`); + return await response.json(); + } catch (error) { + console.error('Error getting Google user info:', error); + throw error; } - - return (await response.json()) as Auth0UserProfile; -} - -export function isAuth0Configured(): boolean { - return Boolean(auth0Domain && auth0Audience); } diff --git a/backend/src/index.ts b/backend/src/index.ts index 25b1d6a..0bdb2fe 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -19,6 +19,8 @@ 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 @@ -46,6 +48,9 @@ app.get('/api/health', (req: Request, res: Response) => { // 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); @@ -606,21 +611,35 @@ app.get('/api/drivers/:driverId/schedule', requireAuth, async (req: Request, res }); // Admin routes -app.get('/api/admin/settings', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => { +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(); - const apiKeys = adminSettings.apiKeys || {}; // Return settings but mask API keys for display only // IMPORTANT: Don't return the actual keys, just indicate they exist const maskedSettings = { apiKeys: { - aviationStackKey: apiKeys.aviationStackKey ? '***' + apiKeys.aviationStackKey.slice(-4) : '', - googleMapsKey: apiKeys.googleMapsKey ? '***' + apiKeys.googleMapsKey.slice(-4) : '', - twilioKey: apiKeys.twilioKey ? '***' + apiKeys.twilioKey.slice(-4) : '', - auth0Domain: apiKeys.auth0Domain ? '***' + apiKeys.auth0Domain.slice(-4) : '', - auth0ClientId: apiKeys.auth0ClientId ? '***' + apiKeys.auth0ClientId.slice(-4) : '', - auth0ClientSecret: apiKeys.auth0ClientSecret ? '***' + apiKeys.auth0ClientSecret.slice(-4) : '' + 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 }; @@ -631,11 +650,16 @@ app.get('/api/admin/settings', requireAuth, requireRole(['administrator']), asyn } }); -app.post('/api/admin/settings', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => { +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(); - currentSettings.apiKeys = currentSettings.apiKeys || {}; // Update API keys (only if provided and not masked) if (apiKeys) { @@ -650,17 +674,15 @@ app.post('/api/admin/settings', requireAuth, requireRole(['administrator']), asy if (apiKeys.twilioKey && !apiKeys.twilioKey.startsWith('***')) { currentSettings.apiKeys.twilioKey = apiKeys.twilioKey; } - if (apiKeys.auth0Domain && !apiKeys.auth0Domain.startsWith('***')) { - currentSettings.apiKeys.auth0Domain = apiKeys.auth0Domain; - process.env.AUTH0_DOMAIN = apiKeys.auth0Domain; + 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.auth0ClientId && !apiKeys.auth0ClientId.startsWith('***')) { - currentSettings.apiKeys.auth0ClientId = apiKeys.auth0ClientId; - process.env.AUTH0_CLIENT_ID = apiKeys.auth0ClientId; - } - if (apiKeys.auth0ClientSecret && !apiKeys.auth0ClientSecret.startsWith('***')) { - currentSettings.apiKeys.auth0ClientSecret = apiKeys.auth0ClientSecret; - process.env.AUTH0_CLIENT_SECRET = apiKeys.auth0ClientSecret; + 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; } } @@ -678,7 +700,13 @@ app.post('/api/admin/settings', requireAuth, requireRole(['administrator']), asy } }); -app.post('/api/admin/test-api/:apiType', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => { +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; @@ -727,6 +755,7 @@ async function startServer() { // 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`); diff --git a/backend/src/routes/simpleAuth.ts b/backend/src/routes/simpleAuth.ts index c4f8915..f3a3511 100644 --- a/backend/src/routes/simpleAuth.ts +++ b/backend/src/routes/simpleAuth.ts @@ -1,176 +1,64 @@ import express, { Request, Response, NextFunction } from 'express'; -import { - fetchAuth0UserProfile, - isAuth0Configured, - verifyAccessToken, - VerifiedAccessToken, - Auth0UserProfile, - getCachedProfile, - cacheAuth0Profile +import { + generateToken, + verifyToken, + getGoogleAuthUrl, + exchangeCodeForTokens, + getGoogleUserInfo, + User } from '../config/simpleAuth'; import databaseService from '../services/databaseService'; -type AuthedRequest = Request & { - auth?: { - token: string; - claims: VerifiedAccessToken; - profile?: Auth0UserProfile | null; - }; - user?: any; -}; - const router = express.Router(); -function mapUserForResponse(user: any) { - return { - id: user.id, - email: user.email, - name: user.name, - picture: user.profile_picture_url, - role: user.role, - approval_status: user.approval_status, - created_at: user.created_at, - last_login: user.last_login, - provider: 'auth0' - }; -} - -async function syncUserWithDatabase(claims: VerifiedAccessToken, token: string): Promise<{ user: any; profile: Auth0UserProfile | null }> { - const auth0Id = claims.sub; - const initialAdminEmails = (process.env.INITIAL_ADMIN_EMAILS || '') - .split(',') - .map(email => email.trim().toLowerCase()) - .filter(Boolean); - - let profile: Auth0UserProfile | null = null; - let user = await databaseService.getUserById(auth0Id); - - if (user) { - const updated = await databaseService.updateUserLastSignIn(user.email); - user = updated || user; - - const isSeedAdmin = initialAdminEmails.includes((user.email || '').toLowerCase()); - if (isSeedAdmin && user.role !== 'administrator') { - user = await databaseService.updateUserRole(user.email, 'administrator'); - } - if (isSeedAdmin && user.approval_status !== 'approved') { - user = await databaseService.updateUserApprovalStatus(user.email, 'approved'); - } - - return { user, profile }; - } - - const cacheKey = auth0Id; - profile = getCachedProfile(cacheKey) || null; - - if (!profile) { - profile = await fetchAuth0UserProfile(token, cacheKey, claims.exp); - cacheAuth0Profile(cacheKey, profile, claims.exp); - } - - if (!profile.email) { - throw new Error('Auth0 profile did not include an email address'); - } - - const existingByEmail = await databaseService.getUserByEmail(profile.email); - - if (existingByEmail && existingByEmail.id !== auth0Id) { - await databaseService.migrateUserId(existingByEmail.id, auth0Id); - user = await databaseService.getUserById(auth0Id); - } else if (existingByEmail) { - user = existingByEmail; - } - - const displayName = profile.name || profile.nickname || profile.email; - const picture = typeof profile.picture === 'string' ? profile.picture : undefined; - const isSeedAdmin = initialAdminEmails.includes(profile.email.toLowerCase()); - - if (!user) { - const approvedUserCount = await databaseService.getApprovedUserCount(); - const role = isSeedAdmin - ? 'administrator' - : approvedUserCount === 0 - ? 'administrator' - : 'coordinator'; - - user = await databaseService.createUser({ - id: auth0Id, - google_id: auth0Id, - email: profile.email, - name: displayName, - profile_picture_url: picture, - role - }); - - if (role === 'administrator') { - user = await databaseService.updateUserApprovalStatus(profile.email, 'approved'); - } - } else { - const updated = await databaseService.updateUserLastSignIn(user.email); - user = updated || user; - - if (isSeedAdmin && user.role !== 'administrator') { - user = await databaseService.updateUserRole(user.email, 'administrator'); - } - if (isSeedAdmin && user.approval_status !== 'approved') { - user = await databaseService.updateUserApprovalStatus(user.email, 'approved'); - } - } - - return { user, profile }; -} - -export async function requireAuth(req: AuthedRequest, res: Response, next: NextFunction) { +// 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); - try { - const claims = await verifyAccessToken(token); - const { user, profile } = await syncUserWithDatabase(claims, token); - - req.auth = { token, claims, profile }; - req.user = user; - - if (user.approval_status !== 'approved') { - return res.status(403).json({ - error: 'pending_approval', - message: 'Your account is pending administrator approval.', - user: mapUserForResponse(user) - }); - } - - return next(); - } catch (error: any) { - console.error('Auth0 token verification failed:', error); - return res.status(401).json({ error: 'Invalid or expired 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: AuthedRequest, res: Response, next: NextFunction) => { - const user = req.user; - + 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(); }; } -router.get('/setup', async (_req: Request, res: Response) => { +// 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: isAuth0Configured(), + setupCompleted: !!(clientId && clientSecret && clientId !== 'your-google-client-id-from-console'), firstAdminCreated: userCount > 0, - oauthConfigured: isAuth0Configured(), - authProvider: 'auth0' + oauthConfigured: !!(clientId && clientSecret) }); } catch (error) { console.error('Error checking setup status:', error); @@ -178,35 +66,206 @@ router.get('/setup', async (_req: Request, res: Response) => { } }); -router.get('/me', requireAuth, async (req: AuthedRequest, res: Response) => { - res.json({ - user: mapUserForResponse(req.user), - auth0: { - sub: req.auth?.claims.sub, - scope: req.auth?.claims.scope - } - }); +// 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`); + } }); -router.post('/logout', (_req: Request, res: Response) => { +// 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' }); }); -router.get('/status', requireAuth, (req: AuthedRequest, res: Response) => { - res.json({ - authenticated: true, - user: mapUserForResponse(req.user) +// 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) => { +router.get('/users', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => { try { const users = await databaseService.getAllUsers(); - - res.json(users.map(mapUserForResponse)); + + 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' }); @@ -230,7 +289,12 @@ router.patch('/users/:email/role', requireAuth, requireRole(['administrator']), res.json({ success: true, - user: mapUserForResponse(user) + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role + } }); } catch (error) { console.error('Error updating user role:', error); @@ -239,9 +303,9 @@ router.patch('/users/:email/role', requireAuth, requireRole(['administrator']), }); // Delete user (admin only) -router.delete('/users/:email', requireAuth, requireRole(['administrator']), async (req: AuthedRequest, res: Response) => { +router.delete('/users/:email', requireAuth, requireRole(['administrator']), async (req: Request, res: Response) => { const { email } = req.params; - const currentUser = req.user; + const currentUser = (req as any).user; // Prevent admin from deleting themselves if (email === currentUser.email) { @@ -272,7 +336,17 @@ router.get('/users/:email', requireAuth, requireRole(['administrator']), async ( return res.status(404).json({ error: 'User not found' }); } - res.json(mapUserForResponse(user)); + 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' }); @@ -286,7 +360,16 @@ router.get('/users/pending/list', requireAuth, requireRole(['administrator']), a try { const pendingUsers = await databaseService.getPendingUsers(); - const userList = pendingUsers.map(mapUserForResponse); + 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) { @@ -313,7 +396,13 @@ router.patch('/users/:email/approval', requireAuth, requireRole(['administrator' res.json({ success: true, message: `User ${status} successfully`, - user: mapUserForResponse(user) + 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); diff --git a/backend/src/services/databaseService.ts b/backend/src/services/databaseService.ts index a05e8ca..ddb5434 100644 --- a/backend/src/services/databaseService.ts +++ b/backend/src/services/databaseService.ts @@ -6,10 +6,9 @@ class DatabaseService { private redis: RedisClientType; constructor() { - const useSSL = process.env.DATABASE_SSL === 'true'; this.pool = new Pool({ connectionString: process.env.DATABASE_URL, - ssl: useSSL ? { rejectUnauthorized: false } : false + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false }); // Initialize Redis connection @@ -98,38 +97,6 @@ class DatabaseService { ADD COLUMN IF NOT EXISTS approval_status VARCHAR(20) DEFAULT 'pending' CHECK (approval_status IN ('pending', 'approved', 'denied')) `); - // Admin settings storage 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 - ) - `); - - 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'; - `); - - await this.query(` - DROP TRIGGER IF EXISTS update_admin_settings_updated_at ON admin_settings - `); - - await this.query(` - CREATE TRIGGER update_admin_settings_updated_at - BEFORE UPDATE ON admin_settings - FOR EACH ROW - EXECUTE FUNCTION update_updated_at_column() - `); - // Create indexes await this.query(` CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id) @@ -191,31 +158,6 @@ class DatabaseService { return result.rows[0] || null; } - async migrateUserId(oldId: string, newId: string): Promise { - const client = await this.pool.connect(); - - try { - await client.query('BEGIN'); - - await client.query( - 'UPDATE drivers SET user_id = $2 WHERE user_id = $1', - [oldId, newId] - ); - - await client.query( - 'UPDATE users SET id = $2, google_id = $2 WHERE id = $1', - [oldId, newId] - ); - - await client.query('COMMIT'); - } catch (error) { - await client.query('ROLLBACK'); - throw error; - } finally { - client.release(); - } - } - async getAllUsers(): Promise { const query = 'SELECT * FROM users ORDER BY created_at ASC'; const result = await this.query(query); @@ -313,29 +255,52 @@ class DatabaseService { } } - // VIP schema (flights, drivers, schedules) + // VIP table initialization using the correct schema async initializeVipTables(): Promise { 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 ) `); - await this.query(` - ALTER TABLE vips - ADD COLUMN IF NOT EXISTS organization VARCHAR(255), - ADD COLUMN IF NOT EXISTS department VARCHAR(255) DEFAULT 'Office of Development', - ADD COLUMN IF NOT EXISTS transport_mode VARCHAR(50), - ADD COLUMN IF NOT EXISTS expected_arrival TIMESTAMP, - ADD COLUMN IF NOT EXISTS needs_airport_pickup BOOLEAN DEFAULT false, - ADD COLUMN IF NOT EXISTS needs_venue_transport BOOLEAN DEFAULT true - `); - + // Create flights table (for VIPs with flight transport) await this.query(` CREATE TABLE IF NOT EXISTS flights ( id SERIAL PRIMARY KEY, @@ -355,22 +320,69 @@ class DatabaseService { ) `); + // 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 ) `); - await this.query(` - ALTER TABLE drivers - ADD COLUMN IF NOT EXISTS phone VARCHAR(50), - ADD COLUMN IF NOT EXISTS department VARCHAR(255) DEFAULT 'Office of Development', - ADD COLUMN IF NOT EXISTS user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL + // 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, @@ -388,42 +400,66 @@ class DatabaseService { ) `); + // Create system_setup table for tracking initial setup await this.query(` - DROP TABLE IF EXISTS schedules + 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(` - ALTER TABLE schedule_events - ADD COLUMN IF NOT EXISTS description TEXT, - ADD COLUMN IF NOT EXISTS assigned_driver_id VARCHAR(255) REFERENCES drivers(id) ON DELETE SET NULL, - ADD COLUMN IF NOT EXISTS status VARCHAR(50) DEFAULT 'scheduled', - ADD COLUMN IF NOT EXISTS event_type VARCHAR(50), - ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + 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 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) + CREATE OR REPLACE FUNCTION update_updated_at_column() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; + END; + $$ language 'plpgsql' `); - console.log('โœ… VIP and schedule tables initialized successfully'); + // 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; } } diff --git a/backend/src/services/enhancedDataService.ts b/backend/src/services/enhancedDataService.ts index a057f9e..543b363 100644 --- a/backend/src/services/enhancedDataService.ts +++ b/backend/src/services/enhancedDataService.ts @@ -589,13 +589,11 @@ class EnhancedDataService { // Default settings structure const defaultSettings = { apiKeys: { - aviationStackKey: process.env.AVIATIONSTACK_API_KEY || '', + aviationStackKey: '', googleMapsKey: '', twilioKey: '', - auth0Domain: process.env.AUTH0_DOMAIN || '', - auth0ClientId: process.env.AUTH0_CLIENT_ID || '', - auth0ClientSecret: process.env.AUTH0_CLIENT_SECRET || '', - auth0Audience: process.env.AUTH0_AUDIENCE || '' + googleClientId: '', + googleClientSecret: '' }, systemSettings: { defaultPickupLocation: '', diff --git a/backend/src/services/flightService.ts b/backend/src/services/flightService.ts index 6a93dcb..2447d27 100644 --- a/backend/src/services/flightService.ts +++ b/backend/src/services/flightService.ts @@ -118,7 +118,7 @@ class FlightService { console.log('Note: Free tier returns recent flights only, not future scheduled flights'); const response = await fetch(url); - const data: any = await response.json(); + const data = await response.json(); console.log('AviationStack response status:', response.status); @@ -128,12 +128,12 @@ class FlightService { } // Check for API errors in response - if (data?.error) { + if (data.error) { console.error('AviationStack API error:', data.error); return null; } - if (Array.isArray(data?.data) && data.data.length > 0) { + if (data.data && data.data.length > 0) { // This is a valid flight number that exists! console.log(`โœ… Valid flight number: ${formattedFlightNumber} exists in the system`); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4633698..0b3e5f1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -37,6 +37,8 @@ services: build: context: ./frontend target: development + environment: + VITE_API_URL: http://localhost:3000/api ports: - 5173:5173 depends_on: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index c728524..a80e167 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -6,55 +6,53 @@ services: image: postgres:15 environment: POSTGRES_DB: vip_coordinator - POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme} volumes: - postgres-data:/var/lib/postgresql/data ports: - 5432:5432 + restart: unless-stopped redis: image: redis:7 ports: - 6379:6379 + restart: unless-stopped backend: build: context: ./backend target: production environment: - DATABASE_URL: postgresql://postgres:${DB_PASSWORD}@db:5432/vip_coordinator + DATABASE_URL: postgresql://postgres:${DB_PASSWORD:-changeme}@db:5432/vip_coordinator REDIS_URL: redis://redis:6379 - NODE_ENV: production - FRONTEND_URL: ${FRONTEND_URL} - AUTH0_DOMAIN: ${AUTH0_DOMAIN} - AUTH0_CLIENT_ID: ${AUTH0_CLIENT_ID} - AUTH0_CLIENT_SECRET: ${AUTH0_CLIENT_SECRET} - AUTH0_AUDIENCE: ${AUTH0_AUDIENCE} - INITIAL_ADMIN_EMAILS: ${INITIAL_ADMIN_EMAILS:-} - AVIATIONSTACK_API_KEY: ${AVIATIONSTACK_API_KEY:-} - DATABASE_SSL: ${DATABASE_SSL:-false} - PGSSLMODE: disable + JWT_SECRET: ${JWT_SECRET:-your-super-secure-jwt-secret-key-change-in-production-12345} + SESSION_SECRET: ${SESSION_SECRET:-your-super-secure-session-secret-change-in-production-67890} + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-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} + GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-https://api.bsa.madeamess.online/auth/google/callback} + FRONTEND_URL: ${FRONTEND_URL:-https://bsa.madeamess.online} + AVIATIONSTACK_API_KEY: ${AVIATIONSTACK_API_KEY:-your-aviationstack-api-key} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123} + PORT: ${PORT:-3000} ports: - 3000:3000 depends_on: - db - redis + restart: unless-stopped frontend: build: context: ./frontend - target: serve - args: - VITE_AUTH0_DOMAIN: ${AUTH0_DOMAIN} - VITE_AUTH0_CLIENT_ID: ${AUTH0_CLIENT_ID} - VITE_AUTH0_AUDIENCE: ${AUTH0_AUDIENCE} + target: production + environment: + VITE_API_URL: ${VITE_API_URL:-https://api.bsa.madeamess.online/api} ports: - - 80:80 - - 443:443 - volumes: - - /opt/vip-coordinator/certs:/etc/nginx/certs:ro + - 5173:5173 depends_on: - backend + restart: unless-stopped volumes: postgres-data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 7526555..db21d6a 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,5 +1,5 @@ # Multi-stage build for development and production -FROM node:18-alpine AS base +FROM node:22-alpine AS base WORKDIR /app @@ -15,20 +15,7 @@ CMD ["npm", "run", "dev"] # Production stage FROM base AS production -ARG VITE_AUTH0_DOMAIN -ARG VITE_AUTH0_CLIENT_ID -ARG VITE_AUTH0_AUDIENCE -ENV VITE_AUTH0_DOMAIN=${VITE_AUTH0_DOMAIN} -ENV VITE_AUTH0_CLIENT_ID=${VITE_AUTH0_CLIENT_ID} -ENV VITE_AUTH0_AUDIENCE=${VITE_AUTH0_AUDIENCE} -RUN npm ci +RUN npm install COPY . . -RUN npm run build -RUN npm prune --omit=dev - -# Serve with nginx -FROM nginx:alpine AS serve -COPY --from=production /app/dist /usr/share/nginx/html -COPY nginx.conf /etc/nginx/nginx.conf -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] +EXPOSE 5173 +CMD ["npm", "run", "dev"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf index b362efd..873189a 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,64 +1,51 @@ -worker_processes auto; - events { - worker_connections 1024; + worker_connections 1024; } http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - - server { - listen 80; - server_name bsa.madeamess.online _; - return 301 https://$host$request_uri; - } - - server { - listen 443 ssl http2; - server_name bsa.madeamess.online _; - - ssl_certificate /etc/nginx/certs/fullchain.pem; - ssl_certificate_key /etc/nginx/certs/privkey.pem; - ssl_prefer_server_ciphers on; - - root /usr/share/nginx/html; - index index.html; - + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + # Gzip compression gzip on; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss image/svg+xml; - gzip_min_length 256; - - location /auth/callback { - try_files $uri /index.html; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Handle client-side routing + location / { + try_files $uri $uri/ /index.html; + } + + # API proxy to backend + location /api/ { + proxy_pass http://backend:3000/api/; + 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; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } } - - location /api/ { - proxy_pass http://backend:3000/api/; - 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_set_header X-Forwarded-Host $host; - } - - location /auth/ { - proxy_pass http://backend:3000/auth/; - 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_set_header X-Forwarded-Host $host; - } - - location / { - try_files $uri $uri/ /index.html; - } - } -} - +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 0b89e5e..908bc2c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,34 +3,38 @@ "private": true, "version": "0.0.0", "type": "module", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.0.0" + }, "scripts": { - "dev": "vite", - "build": "vite build", + "dev": "node ./node_modules/vite/bin/vite.js", + "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { - "@auth0/auth0-react": "^2.8.0", "leaflet": "^1.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-leaflet": "^4.2.1", - "react-router-dom": "^6.15.0", - "tailwindcss": "^3.4.14", - "vite": "^4.5.14" + "react-router-dom": "^6.15.0" }, "devDependencies": { "@types/leaflet": "^1.9.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "@vitejs/plugin-react": "^4.0.3", - "autoprefixer": "^10.4.21", - "eslint": "^8.45.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "postcss": "^8.4.47", - "typescript": "^5.0.2" + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", + "@vitejs/plugin-react": "^4.3.3", + "autoprefixer": "^10.4.14", + "eslint": "^9.15.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "@tailwindcss/postcss": "^4.1.8", + "postcss": "^8.5.4", + "tailwindcss": "^4.1.8", + "typescript": "^5.6.0", + "vite": "^5.4.10" } } diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 0000000..8416a01 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + } +} \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css index d50b51b..0b0ce05 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,105 +1,117 @@ /* Modern App-specific styles using Tailwind utilities */ /* Enhanced button styles */ -.btn-modern { - @apply px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5; -} - -.btn-gradient-blue { - @apply bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white; -} - -.btn-gradient-green { - @apply bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white; -} - -.btn-gradient-purple { - @apply bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white; -} - -.btn-gradient-amber { - @apply bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-600 hover:to-amber-700 text-white; +@layer components { + .btn-modern { + @apply px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5; + } + + .btn-gradient-blue { + @apply bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white; + } + + .btn-gradient-green { + @apply bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white; + } + + .btn-gradient-purple { + @apply bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 text-white; + } + + .btn-gradient-amber { + @apply bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-600 hover:to-amber-700 text-white; + } } /* Status badges */ -.status-badge { - @apply inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold; -} +@layer components { + .status-badge { + @apply inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold; + } -.status-scheduled { - @apply bg-blue-100 text-blue-800 border border-blue-200; -} + .status-scheduled { + @apply bg-blue-100 text-blue-800 border border-blue-200; + } -.status-in-progress { - @apply bg-amber-100 text-amber-800 border border-amber-200; -} + .status-in-progress { + @apply bg-amber-100 text-amber-800 border border-amber-200; + } -.status-completed { - @apply bg-green-100 text-green-800 border border-green-200; -} + .status-completed { + @apply bg-green-100 text-green-800 border border-green-200; + } -.status-cancelled { - @apply bg-red-100 text-red-800 border border-red-200; + .status-cancelled { + @apply bg-red-100 text-red-800 border border-red-200; + } } /* Card enhancements */ -.card-modern { - @apply bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden backdrop-blur-sm; -} - -.card-header { - @apply bg-gradient-to-r from-slate-50 to-slate-100 px-6 py-4 border-b border-slate-200/60; -} - -.card-content { - @apply p-6; +@layer components { + .card-modern { + @apply bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden backdrop-blur-sm; + } + + .card-header { + @apply bg-gradient-to-r from-slate-50 to-slate-100 px-6 py-4 border-b border-slate-200/60; + } + + .card-content { + @apply p-6; + } } /* Loading states */ -.loading-spinner { - @apply animate-spin rounded-full border-4 border-blue-600 border-t-transparent; -} +@layer components { + .loading-spinner { + @apply animate-spin rounded-full border-4 border-blue-600 border-t-transparent; + } -.loading-text { - @apply text-slate-600 animate-pulse; -} - -.skeleton { - @apply animate-pulse bg-slate-200 rounded; + .loading-text { + @apply text-slate-600 animate-pulse; + } + + .skeleton { + @apply animate-pulse bg-slate-200 rounded; + } } /* Form enhancements */ -.form-modern { - @apply space-y-6; -} - -.form-group-modern { - @apply space-y-2; -} - -.form-label-modern { - @apply block text-sm font-semibold text-slate-700; -} - -.form-input-modern { - @apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200; -} - -.form-select-modern { - @apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white transition-all duration-200; +@layer components { + .form-modern { + @apply space-y-6; + } + + .form-group-modern { + @apply space-y-2; + } + + .form-label-modern { + @apply block text-sm font-semibold text-slate-700; + } + + .form-input-modern { + @apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200; + } + + .form-select-modern { + @apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white transition-all duration-200; + } } /* Animation utilities */ -.animate-fade-in { - animation: fadeIn 0.5s ease-in-out; -} - -.animate-slide-up { - animation: slideUp 0.3s ease-out; -} - -.animate-scale-in { - animation: scaleIn 0.2s ease-out; +@layer utilities { + .animate-fade-in { + animation: fadeIn 0.5s ease-in-out; + } + + .animate-slide-up { + animation: slideUp 0.3s ease-out; + } + + .animate-scale-in { + animation: scaleIn 0.2s ease-out; + } } @keyframes fadeIn { @@ -149,23 +161,27 @@ } /* Glass morphism effect */ -.glass { - @apply bg-white/80 backdrop-blur-lg border border-white/20; -} - -.glass-dark { - @apply bg-slate-900/80 backdrop-blur-lg border border-slate-700/20; +@layer utilities { + .glass { + @apply bg-white/80 backdrop-blur-lg border border-white/20; + } + + .glass-dark { + @apply bg-slate-900/80 backdrop-blur-lg border border-slate-700/20; + } } /* Hover effects */ -.hover-lift { - @apply transition-transform duration-200 hover:-translate-y-1; -} - -.hover-glow { - @apply transition-shadow duration-200 hover:shadow-2xl; -} - -.hover-scale { - @apply transition-transform duration-200 hover:scale-105; +@layer utilities { + .hover-lift { + @apply transition-transform duration-200 hover:-translate-y-1; + } + + .hover-glow { + @apply transition-shadow duration-200 hover:shadow-2xl; + } + + .hover-scale { + @apply transition-transform duration-200 hover:scale-105; + } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7cc99fd..e4de022 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; -import { useAuth0 } from '@auth0/auth0-react'; import { apiCall } from './config/api'; import VipList from './pages/VipList'; import VipDetails from './pages/VipDetails'; @@ -12,102 +11,54 @@ import UserManagement from './components/UserManagement'; import Login from './components/Login'; import './App.css'; -const AUTH0_AUDIENCE = import.meta.env.VITE_AUTH0_AUDIENCE; - function App() { - const { - isLoading: authLoading, - isAuthenticated, - loginWithRedirect, - logout, - getAccessTokenSilently, - user: auth0User, - error: authError - } = useAuth0(); - const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); - const [statusMessage, setStatusMessage] = useState(null); - const [pendingApproval, setPendingApproval] = useState(false); useEffect(() => { - const bootstrap = async () => { - if (!isAuthenticated) { - setUser(null); - setStatusMessage(null); - setPendingApproval(false); - setLoading(false); - return; - } - - setLoading(true); - setPendingApproval(false); - setStatusMessage(null); - - try { - const token = await getAccessTokenSilently({ - authorizationParams: { - ...(AUTH0_AUDIENCE ? { audience: AUTH0_AUDIENCE } : {}), - scope: 'openid profile email' - } - }); - - localStorage.setItem('authToken', token); - - const response = await apiCall('/auth/me', { - headers: { - Authorization: `Bearer ${token}` - } - }); - - if (response.status === 403) { - const data = await response.json(); - setUser(null); - setPendingApproval(true); - setStatusMessage(data.message || 'Your account is pending administrator approval.'); - return; + // Check if user is already authenticated + const token = localStorage.getItem('authToken'); + if (token) { + apiCall('/auth/me', { + headers: { + 'Authorization': `Bearer ${token}` } - - if (!response.ok) { - throw new Error(`Failed to load profile (${response.status})`); + }) + .then(res => { + if (res.ok) { + return res.json(); + } else { + // Token is invalid, remove it + localStorage.removeItem('authToken'); + throw new Error('Invalid token'); } - - const data = await response.json(); - const userRecord = data.user || data; - - const resolvedName = - userRecord.name || - auth0User?.name || - auth0User?.nickname || - auth0User?.email || - userRecord.email; - - setUser({ - ...userRecord, - name: resolvedName, - role: userRecord.role, - picture: userRecord.picture || auth0User?.picture - }); - } catch (error) { - console.error('Authentication bootstrap failed:', error); - setUser(null); - setStatusMessage('Authentication failed. Please try signing in again.'); - } finally { + }) + .then(userData => { + setUser(userData); setLoading(false); - } - }; - - if (!authLoading) { - bootstrap(); + }) + .catch(error => { + console.error('Auth check failed:', error); + setLoading(false); + }); + } else { + setLoading(false); } - }, [isAuthenticated, authLoading, getAccessTokenSilently, auth0User]); + }, []); + + const handleLogin = (userData: any) => { + setUser(userData); + }; const handleLogout = () => { localStorage.removeItem('authToken'); - logout({ logoutParams: { returnTo: window.location.origin } }); + setUser(null); + // Optionally call logout endpoint + apiCall('/auth/logout', { method: 'POST' }) + .catch(error => console.error('Logout error:', error)); }; - if (authLoading || loading) { + if (loading) { return (
@@ -118,68 +69,23 @@ function App() { ); } - if (pendingApproval) { - return ( -
-
-
-
- โณ -
-
-

Awaiting Administrator Approval

-

- {statusMessage || - 'Thanks for signing in. An administrator needs to approve your account before you can access the dashboard.'} -

- -
-
- ); + // Handle OAuth callback route even when not logged in + if (window.location.pathname === '/auth/callback' || window.location.pathname === '/auth/google/callback') { + return ; } - const beginLogin = async () => { - try { - await loginWithRedirect({ - authorizationParams: { - ...(AUTH0_AUDIENCE ? { audience: AUTH0_AUDIENCE } : {}), - scope: 'openid profile email', - redirect_uri: `${window.location.origin}/auth/callback` - } - }); - } catch (error: any) { - console.error('Auth0 login failed:', error); - setStatusMessage(error?.message || 'Authentication failed. Please try again.'); - } - }; - - if (!isAuthenticated || !user) { - return ( - - ); + if (!user) { + return ; } - const displayName = - (user.name && user.name.trim().length > 0) - ? user.name - : (user.email || 'User'); - const displayInitial = displayName.trim().charAt(0).toUpperCase(); - const userRole = user.role || 'user'; - return (
+ {/* Modern Navigation */} + {/* Main Content */}
} /> diff --git a/frontend/src/components/Login.css b/frontend/src/components/Login.css index f529018..7d50d91 100644 --- a/frontend/src/components/Login.css +++ b/frontend/src/components/Login.css @@ -94,101 +94,6 @@ margin: 0; } -.dev-login-divider { - display: flex; - align-items: center; - gap: 12px; - margin: 24px 0; - color: #94a3b8; - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.dev-login-divider::before, -.dev-login-divider::after { - content: ''; - flex: 1; - height: 1px; - background: #e2e8f0; -} - -.dev-login-form { - display: flex; - flex-direction: column; - gap: 16px; - text-align: left; -} - -.dev-login-form h3 { - margin: 0; - color: #1e293b; - font-size: 18px; - font-weight: 600; -} - -.dev-login-hint { - margin: 0; - font-size: 13px; - color: #64748b; - line-height: 1.4; -} - -.dev-login-form label { - display: flex; - flex-direction: column; - gap: 6px; - font-size: 14px; - color: #334155; -} - -.dev-login-form input { - width: 100%; - padding: 10px 12px; - border: 1px solid #cbd5f5; - border-radius: 8px; - font-size: 14px; - transition: border-color 0.2s ease, box-shadow 0.2s ease; -} - -.dev-login-form input:focus { - outline: none; - border-color: #2563eb; - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); -} - -.dev-login-error { - background: #fee2e2; - border: 1px solid #fecaca; - color: #dc2626; - padding: 10px 12px; - border-radius: 8px; - font-size: 13px; -} - -.dev-login-button { - width: 100%; - padding: 12px; - border: none; - border-radius: 8px; - background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); - color: white; - font-size: 15px; - font-weight: 600; - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.dev-login-button:hover:not(:disabled) { - transform: translateY(-1px); - box-shadow: 0 10px 20px rgba(37, 99, 235, 0.2); -} - -.dev-login-button:disabled { - opacity: 0.7; - cursor: not-allowed; -} - .login-footer { border-top: 1px solid #eee; padding-top: 20px; diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index fe3f73a..c83a753 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -3,15 +3,15 @@ import { apiCall } from '../config/api'; import './Login.css'; interface LoginProps { - onLogin: () => void; - errorMessage?: string | null | undefined; + onLogin: (user: any) => void; } -const Login: React.FC = ({ onLogin, errorMessage }) => { +const Login: React.FC = ({ onLogin }) => { const [setupStatus, setSetupStatus] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { + // Check system setup status apiCall('/auth/setup') .then(res => res.json()) .then(data => { @@ -22,7 +22,82 @@ const Login: React.FC = ({ onLogin, errorMessage }) => { console.error('Error checking setup status:', error); setLoading(false); }); - }, []); + + // Check for OAuth callback code in URL + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + const error = urlParams.get('error'); + const token = urlParams.get('token'); + + if (code && (window.location.pathname === '/auth/google/callback' || window.location.pathname === '/auth/callback')) { + // Exchange code for token + apiCall('/auth/google/exchange', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ code }) + }) + .then(res => { + if (!res.ok) { + throw new Error('Failed to exchange code for token'); + } + return res.json(); + }) + .then(({ token, user }) => { + localStorage.setItem('authToken', token); + onLogin(user); + // Clean up URL and redirect to dashboard + window.history.replaceState({}, document.title, '/'); + }) + .catch(error => { + console.error('OAuth exchange failed:', error); + alert('Login failed. Please try again.'); + // Clean up URL + window.history.replaceState({}, document.title, '/'); + }); + } else if (token && (window.location.pathname === '/auth/callback' || window.location.pathname === '/auth/google/callback')) { + // Direct token from URL (from backend redirect) + localStorage.setItem('authToken', token); + + apiCall('/auth/me', { + headers: { + 'Authorization': `Bearer ${token}` + } + }) + .then(res => res.json()) + .then(user => { + onLogin(user); + // Clean up URL and redirect to dashboard + window.history.replaceState({}, document.title, '/'); + }) + .catch(error => { + console.error('Error getting user info:', error); + localStorage.removeItem('authToken'); + // Clean up URL + window.history.replaceState({}, document.title, '/'); + }); + } else if (error) { + console.error('Authentication error:', error); + alert(`Login error: ${error}`); + // Clean up URL + window.history.replaceState({}, document.title, '/'); + } + }, [onLogin]); + + const handleGoogleLogin = async () => { + try { + // Get OAuth URL from backend + const response = await apiCall('/auth/google/url'); + const { url } = await response.json(); + + // Redirect to Google OAuth + window.location.href = url; + } catch (error) { + console.error('Failed to get OAuth URL:', error); + alert('Login failed. Please try again.'); + } + }; if (loading) { return ( @@ -45,38 +120,55 @@ const Login: React.FC = ({ onLogin, errorMessage }) => { {!setupStatus?.firstAdminCreated && (

๐Ÿš€ First Time Setup

-

The first person to sign in will be promoted to administrator automatically.

+

The first person to log in will become the system administrator.

)}
-

- {setupStatus?.authProvider === 'auth0' - ? 'Sign in with your organisation account. We use Auth0 for secure authentication.' - : 'Authentication service is being configured. Please try again later.'} + {setupStatus?.firstAdminCreated + ? "Sign in with your Google account to access the VIP Coordinator." + : "Sign in with Google to set up your administrator account." + }

- {errorMessage && ( -
- {errorMessage} + {setupStatus && !setupStatus.setupCompleted && ( +
+ โš ๏ธ Setup Required: +

+ Google OAuth credentials need to be configured. If the login doesn't work, + please follow the setup guide in GOOGLE_OAUTH_SETUP.md to configure + your Google Cloud Console credentials in the admin dashboard. +

)}
-

Secure authentication powered by Auth0

+

Secure authentication powered by Google OAuth

diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts index 4eb8e07..bbe5641 100644 --- a/frontend/src/config/api.ts +++ b/frontend/src/config/api.ts @@ -1,14 +1,9 @@ -const DEFAULT_API_BASE = - typeof window !== 'undefined' - ? window.location.origin - : 'http://localhost:3000'; - -export const API_BASE_URL = - import.meta.env.VITE_API_URL?.replace(/\/$/, '') || DEFAULT_API_BASE; +// API Configuration +// Use environment variable with fallback to production URL +export const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.bsa.madeamess.online'; +// Helper function for API calls export const apiCall = (endpoint: string, options?: RequestInit) => { - const url = /^https?:\/\//.test(endpoint) - ? endpoint - : `${API_BASE_URL}${endpoint}`; + const url = endpoint.startsWith('/') ? `${API_BASE_URL}${endpoint}` : endpoint; return fetch(url, options); }; diff --git a/frontend/src/index.css b/frontend/src/index.css index 94354b6..4d7ddbd 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,49 +1,4 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -/* Custom base styles */ -@layer base { - :root { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; - line-height: 1.6; - font-weight: 400; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; - } - - * { - box-sizing: border-box; - } - - body { - margin: 0; - min-width: 320px; - min-height: 100vh; - background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); - color: #1e293b; - } - - #root { - width: 100%; - margin: 0 auto; - text-align: left; - } - - /* Smooth scrolling */ - html { - scroll-behavior: smooth; - } - - /* Focus styles */ - *:focus { - outline: 2px solid #3b82f6; - outline-offset: 2px; - } -} +@import "tailwindcss"; /* Custom base styles */ @layer base { @@ -92,117 +47,304 @@ @layer components { /* Modern Button Styles */ .btn { - @apply px-6 py-3 rounded-xl font-semibold text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5; + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: 0.75rem; + padding-bottom: 0.75rem; + border-radius: 0.75rem; + font-weight: 600; + font-size: 0.875rem; + transition: all 0.2s; + outline: none; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + transform: translateY(0); + } + + .btn:focus { + ring: 2px; + ring-offset: 2px; + } + + .btn:hover { + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + transform: translateY(-0.125rem); } .btn-primary { - @apply bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white focus:ring-blue-500; + background: linear-gradient(to right, #3b82f6, #2563eb); + color: white; + } + + .btn-primary:hover { + background: linear-gradient(to right, #2563eb, #1d4ed8); + } + + .btn-primary:focus { + ring-color: #3b82f6; } .btn-secondary { - @apply bg-gradient-to-r from-slate-500 to-slate-600 hover:from-slate-600 hover:to-slate-700 text-white focus:ring-slate-500; + background: linear-gradient(to right, #64748b, #475569); + color: white; + } + + .btn-secondary:hover { + background: linear-gradient(to right, #475569, #334155); + } + + .btn-secondary:focus { + ring-color: #64748b; } .btn-danger { - @apply bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white focus:ring-red-500; + background: linear-gradient(to right, #ef4444, #dc2626); + color: white; + } + + .btn-danger:hover { + background: linear-gradient(to right, #dc2626, #b91c1c); + } + + .btn-danger:focus { + ring-color: #ef4444; } .btn-success { - @apply bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white focus:ring-green-500; + background: linear-gradient(to right, #22c55e, #16a34a); + color: white; + } + + .btn-success:hover { + background: linear-gradient(to right, #16a34a, #15803d); + } + + .btn-success:focus { + ring-color: #22c55e; } /* Modern Card Styles */ .card { - @apply bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden backdrop-blur-sm; + background-color: white; + border-radius: 1rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + border: 1px solid rgba(226, 232, 240, 0.6); + overflow: hidden; + backdrop-filter: blur(4px); } /* Modern Form Styles */ .form-group { - @apply mb-6; + margin-bottom: 1.5rem; } .form-label { - @apply block text-sm font-semibold text-slate-700 mb-3; + display: block; + font-size: 0.875rem; + font-weight: 600; + color: #334155; + margin-bottom: 0.75rem; } .form-input { - @apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white; + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid #cbd5e1; + border-radius: 0.75rem; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + background-color: white; + transition: all 0.2s; + } + + .form-input:focus { + outline: none; + ring: 2px; + ring-color: #3b82f6; + border-color: #3b82f6; } .form-select { - @apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white transition-all duration-200; + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid #cbd5e1; + border-radius: 0.75rem; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + background-color: white; + transition: all 0.2s; + } + + .form-select:focus { + outline: none; + ring: 2px; + ring-color: #3b82f6; + border-color: #3b82f6; } .form-textarea { - @apply w-full px-4 py-3 border border-slate-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 bg-white resize-none; + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid #cbd5e1; + border-radius: 0.75rem; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + background-color: white; + transition: all 0.2s; + resize: none; + } + + .form-textarea:focus { + outline: none; + ring: 2px; + ring-color: #3b82f6; + border-color: #3b82f6; } .form-checkbox { - @apply w-5 h-5 text-blue-600 border-slate-300 rounded focus:ring-blue-500 focus:ring-2; + width: 1.25rem; + height: 1.25rem; + color: #2563eb; + border: 1px solid #cbd5e1; + border-radius: 0.25rem; + } + + .form-checkbox:focus { + ring: 2px; + ring-color: #3b82f6; } .form-radio { - @apply w-4 h-4 text-blue-600 border-slate-300 focus:ring-blue-500 focus:ring-2; + width: 1rem; + height: 1rem; + color: #2563eb; + border: 1px solid #cbd5e1; + } + + .form-radio:focus { + ring: 2px; + ring-color: #3b82f6; } /* Modal Styles */ .modal-overlay { - @apply fixed inset-0 bg-black/50 backdrop-blur-sm flex justify-center items-center z-50 p-4; + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + justify-content: center; + align-items: center; + z-index: 50; + padding: 1rem; } .modal-content { - @apply bg-white rounded-2xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto; + background-color: white; + border-radius: 1rem; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + max-width: 56rem; + width: 100%; + max-height: 90vh; + overflow-y: auto; } .modal-header { - @apply bg-gradient-to-r from-blue-50 to-indigo-50 px-8 py-6 border-b border-slate-200/60; + background: linear-gradient(to right, #eff6ff, #eef2ff); + padding: 1.5rem 2rem; + border-bottom: 1px solid rgba(226, 232, 240, 0.6); } .modal-body { - @apply p-8; + padding: 2rem; } .modal-footer { - @apply bg-slate-50 px-8 py-6 border-t border-slate-200/60 flex justify-end space-x-4; + background-color: #f8fafc; + padding: 1.5rem 2rem; + border-top: 1px solid rgba(226, 232, 240, 0.6); + display: flex; + justify-content: flex-end; + gap: 1rem; } /* Form Actions */ .form-actions { - @apply flex justify-end space-x-4 pt-6 border-t border-slate-200/60 mt-8; + display: flex; + justify-content: flex-end; + gap: 1rem; + padding-top: 1.5rem; + border-top: 1px solid rgba(226, 232, 240, 0.6); + margin-top: 2rem; } /* Form Sections */ .form-section { - @apply bg-slate-50 rounded-xl p-6 mb-6 border border-slate-200/60; + background-color: #f8fafc; + border-radius: 0.75rem; + padding: 1.5rem; + margin-bottom: 1.5rem; + border: 1px solid rgba(226, 232, 240, 0.6); } .form-section-header { - @apply flex items-center justify-between mb-4; + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; } .form-section-title { - @apply text-lg font-bold text-slate-800; + font-size: 1.125rem; + font-weight: 700; + color: #1e293b; } /* Radio Group */ .radio-group { - @apply flex gap-6 mt-3; + display: flex; + gap: 1.5rem; + margin-top: 0.75rem; } .radio-option { - @apply flex items-center cursor-pointer bg-white rounded-lg px-4 py-3 border border-slate-200 hover:border-blue-300 hover:bg-blue-50 transition-all duration-200; + display: flex; + align-items: center; + cursor: pointer; + background-color: white; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + border: 1px solid #e2e8f0; + transition: all 0.2s; + } + + .radio-option:hover { + border-color: #93c5fd; + background-color: #eff6ff; } .radio-option.selected { - @apply border-blue-500 bg-blue-50 ring-2 ring-blue-200; + border-color: #3b82f6; + background-color: #eff6ff; + ring: 2px; + ring-color: #bfdbfe; } /* Checkbox Group */ .checkbox-option { - @apply flex items-center cursor-pointer bg-white rounded-lg px-4 py-3 border border-slate-200 hover:border-blue-300 hover:bg-blue-50 transition-all duration-200; + display: flex; + align-items: center; + cursor: pointer; + background-color: white; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + border: 1px solid #e2e8f0; + transition: all 0.2s; + } + + .checkbox-option:hover { + border-color: #93c5fd; + background-color: #eff6ff; } .checkbox-option.checked { - @apply border-blue-500 bg-blue-50; + border-color: #3b82f6; + background-color: #eff6ff; } } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 050edcc..3d7150d 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,36 +1,10 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { Auth0Provider } from '@auth0/auth0-react'; -import App from './App.tsx'; -import './index.css'; - -const domain = import.meta.env.VITE_AUTH0_DOMAIN; -const clientId = import.meta.env.VITE_AUTH0_CLIENT_ID; -const audience = import.meta.env.VITE_AUTH0_AUDIENCE; - -if (!domain || !clientId) { - throw new Error('Auth0 environment variables are missing. Please set VITE_AUTH0_DOMAIN and VITE_AUTH0_CLIENT_ID.'); -} - -const authorizationParams: Record = { - redirect_uri: `${window.location.origin}/auth/callback`, - scope: 'openid profile email' -}; - -if (audience) { - authorizationParams.audience = audience; -} +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( - - - - -); + + , +) diff --git a/frontend/src/pages/AdminDashboard.tsx b/frontend/src/pages/AdminDashboard.tsx index f0596a8..3dc0822 100644 --- a/frontend/src/pages/AdminDashboard.tsx +++ b/frontend/src/pages/AdminDashboard.tsx @@ -1,16 +1,14 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { apiCall, API_BASE_URL } from '../config/api'; +import { apiCall } from '../config/api'; import { generateTestVips, getTestOrganizations, generateVipSchedule } from '../utils/testVipData'; interface ApiKeys { aviationStackKey?: string; googleMapsKey?: string; twilioKey?: string; - auth0Domain?: string; - auth0ClientId?: string; - auth0ClientSecret?: string; - auth0Audience?: string; + googleClientId?: string; + googleClientSecret?: string; } interface SystemSettings { @@ -22,89 +20,92 @@ interface SystemSettings { const AdminDashboard: React.FC = () => { const navigate = useNavigate(); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [adminPassword, setAdminPassword] = useState(''); const [apiKeys, setApiKeys] = useState({}); const [systemSettings, setSystemSettings] = useState({}); const [testResults, setTestResults] = useState<{ [key: string]: string }>({}); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); + const [loading, setLoading] = useState(false); const [saveStatus, setSaveStatus] = useState(null); + const [showKeys, setShowKeys] = useState<{ [key: string]: boolean }>({}); const [savedKeys, setSavedKeys] = useState<{ [key: string]: boolean }>({}); - const [maskedKeyHints, setMaskedKeyHints] = useState<{ [key: string]: string }>({}); const [testDataLoading, setTestDataLoading] = useState(false); const [testDataStatus, setTestDataStatus] = useState(null); - const [error, setError] = useState(null); - const buildAuthHeaders = (includeJson = false) => { - const headers: Record = {}; - const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null; - if (token) { - headers['Authorization'] = `Bearer ${token}`; + useEffect(() => { + // Check if already authenticated + const authStatus = sessionStorage.getItem('adminAuthenticated'); + if (authStatus === 'true') { + setIsAuthenticated(true); + loadSettings(); } - if (includeJson) { - headers['Content-Type'] = 'application/json'; + }, []); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + const response = await fetch('/api/admin/authenticate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password: adminPassword }) + }); + + if (response.ok) { + setIsAuthenticated(true); + sessionStorage.setItem('adminAuthenticated', 'true'); + loadSettings(); + } else { + alert('Invalid admin password'); + } + } catch (error) { + alert('Authentication failed'); } - return headers; }; const loadSettings = async () => { try { - setLoading(true); - setError(null); - const response = await apiCall('/api/admin/settings', { - headers: buildAuthHeaders() + const response = await fetch('/api/admin/settings', { + headers: { + 'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || '' + } }); - + if (response.ok) { const data = await response.json(); - + // Track which keys are already saved (masked keys start with ***) const saved: { [key: string]: boolean } = {}; - const maskedHints: { [key: string]: string } = {}; - const cleanedApiKeys: ApiKeys = {}; - if (data.apiKeys) { Object.entries(data.apiKeys).forEach(([key, value]) => { if (value && (value as string).startsWith('***')) { saved[key] = true; - maskedHints[key] = value as string; - } else if (value) { + } + }); + } + setSavedKeys(saved); + // Don't load masked keys as actual values - keep them empty + const cleanedApiKeys: ApiKeys = {}; + if (data.apiKeys) { + Object.entries(data.apiKeys).forEach(([key, value]) => { + // Only set the value if it's not a masked key + if (value && !(value as string).startsWith('***')) { cleanedApiKeys[key as keyof ApiKeys] = value as string; } }); } - - setSavedKeys(saved); - setMaskedKeyHints(maskedHints); setApiKeys(cleanedApiKeys); setSystemSettings(data.systemSettings || {}); - } else if (response.status === 403) { - setError('You need administrator access to view this page.'); - } else if (response.status === 401) { - setError('Authentication required. Please sign in again.'); - } else { - setError('Failed to load admin settings.'); } - } catch (err) { - console.error('Failed to load settings:', err); - setError('Failed to load admin settings.'); - } finally { - setLoading(false); + } catch (error) { + console.error('Failed to load settings:', error); } }; - useEffect(() => { - loadSettings(); - }, []); - const handleApiKeyChange = (key: keyof ApiKeys, value: string) => { setApiKeys(prev => ({ ...prev, [key]: value })); // If user is typing a new key, mark it as not saved anymore if (value && !value.startsWith('***')) { setSavedKeys(prev => ({ ...prev, [key]: false })); - setMaskedKeyHints(prev => { - const next = { ...prev }; - delete next[key]; - return next; - }); } }; @@ -116,9 +117,12 @@ const AdminDashboard: React.FC = () => { setTestResults(prev => ({ ...prev, [apiType]: 'Testing...' })); try { - const response = await apiCall(`/api/admin/test-api/${apiType}`, { + const response = await fetch(`/api/admin/test-api/${apiType}`, { method: 'POST', - headers: buildAuthHeaders(true), + headers: { + 'Content-Type': 'application/json', + 'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || '' + }, body: JSON.stringify({ apiKey: apiKeys[apiType as keyof ApiKeys] }) @@ -146,13 +150,16 @@ const AdminDashboard: React.FC = () => { }; const saveSettings = async () => { - setSaving(true); + setLoading(true); setSaveStatus(null); try { - const response = await apiCall('/api/admin/settings', { + const response = await fetch('/api/admin/settings', { method: 'POST', - headers: buildAuthHeaders(true), + headers: { + 'Content-Type': 'application/json', + 'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || '' + }, body: JSON.stringify({ apiKeys, systemSettings @@ -161,8 +168,16 @@ const AdminDashboard: React.FC = () => { if (response.ok) { setSaveStatus('Settings saved successfully!'); - // Refresh the latest settings so saved states/labels stay accurate - await loadSettings(); + // Mark keys as saved if they have values + const newSavedKeys: { [key: string]: boolean } = {}; + Object.entries(apiKeys).forEach(([key, value]) => { + if (value && !value.startsWith('***')) { + newSavedKeys[key] = true; + } + }); + setSavedKeys(prev => ({ ...prev, ...newSavedKeys })); + // Clear the input fields after successful save + setApiKeys({}); setTimeout(() => setSaveStatus(null), 3000); } else { setSaveStatus('Failed to save settings'); @@ -170,14 +185,14 @@ const AdminDashboard: React.FC = () => { } catch (error) { setSaveStatus('Error saving settings'); } finally { - setSaving(false); + setLoading(false); } }; const handleLogout = () => { - localStorage.removeItem('authToken'); + sessionStorage.removeItem('adminAuthenticated'); + setIsAuthenticated(false); navigate('/'); - window.location.reload(); }; // Test VIP functions @@ -337,29 +352,37 @@ const AdminDashboard: React.FC = () => { } }; - if (loading) { + if (!isAuthenticated) { return (
-
-
- Loading admin settings... -
-
- ); - } - - if (error) { - return ( -
-
-

Admin access required

-

{error}

- +
+
+
+
+
+
+
+

Admin Login

+

Enter your admin password to continue

+
+ +
+
+ + setAdminPassword(e.target.value)} + className="form-input" + placeholder="Enter admin password" + required + /> +
+ +
); @@ -415,20 +438,24 @@ const AdminDashboard: React.FC = () => {
- handleApiKeyChange('aviationStackKey', e.target.value)} - className="form-input" - /> - {savedKeys.aviationStackKey && maskedKeyHints.aviationStackKey && !apiKeys.aviationStackKey && ( -

- Currently saved key ends with {maskedKeyHints.aviationStackKey.slice(-4)}. Enter a new value to replace it. -

- )} +
+ handleApiKeyChange('aviationStackKey', e.target.value)} + className="form-input pr-12" + /> + {savedKeys.aviationStackKey && ( + + )} +

Get your key from: https://aviationstack.com/dashboard

@@ -457,91 +484,72 @@ const AdminDashboard: React.FC = () => {
- {/* Auth0 Credentials */} + {/* Google OAuth Credentials */}
-

Auth0 Configuration

- {(savedKeys.auth0Domain || savedKeys.auth0ClientId || savedKeys.auth0ClientSecret) && ( +

Google OAuth Credentials

+ {(savedKeys.googleClientId && savedKeys.googleClientSecret) && ( Configured )}
- +
-
- - handleApiKeyChange('auth0Domain', e.target.value)} - className="form-input" - /> -
-
- handleApiKeyChange('auth0ClientId', e.target.value)} - className="form-input" - /> - {savedKeys.auth0ClientId && maskedKeyHints.auth0ClientId && !apiKeys.auth0ClientId && ( -

- Saved client ID ends with {maskedKeyHints.auth0ClientId.slice(-4)}. Provide a new ID to update it. -

- )} +
+ handleApiKeyChange('googleClientId', e.target.value)} + className="form-input pr-12" + /> + {savedKeys.googleClientId && ( + + )} +
- handleApiKeyChange('auth0ClientSecret', e.target.value)} - className="form-input" - /> - {savedKeys.auth0ClientSecret && maskedKeyHints.auth0ClientSecret && !apiKeys.auth0ClientSecret && ( -

- Saved client secret ends with {maskedKeyHints.auth0ClientSecret.slice(-4)}. Provide a new secret to rotate it. -

- )} -
- -
- - handleApiKeyChange('auth0Audience', e.target.value)} - className="form-input" - /> -

- Create an API in Auth0 and use its Identifier here (e.g. https://vip-coordinator-api). -

+
+ handleApiKeyChange('googleClientSecret', e.target.value)} + className="form-input pr-12" + /> + {savedKeys.googleClientSecret && ( + + )} +
- +

Setup Instructions

    -
  1. Sign in to the Auth0 Dashboard
  2. -
  3. Create a Single Page Application for the frontend
  4. -
  5. Set Allowed Callback URL to https://bsa.madeamess.online/auth/callback
  6. -
  7. Set Allowed Logout URL to https://bsa.madeamess.online/
  8. -
  9. Set Allowed Web Origins to https://bsa.madeamess.online
  10. -
  11. Create an API in Auth0 for the backend and use its Identifier as the audience
  12. +
  13. Go to Google Cloud Console
  14. +
  15. Create or select a project
  16. +
  17. Enable the Google+ API
  18. +
  19. Go to "Credentials" โ†’ "Create Credentials" โ†’ "OAuth 2.0 Client IDs"
  20. +
  21. Set authorized redirect URI: http://bsa.madeamess.online:3000/auth/google/callback
  22. +
  23. Set authorized JavaScript origins: http://bsa.madeamess.online:5173
@@ -751,7 +759,7 @@ const AdminDashboard: React.FC = () => {

@@ -803,9 +811,9 @@ const AdminDashboard: React.FC = () => { {saveStatus && ( diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index bb18714..89a305e 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -8,5 +8,4 @@ export default { extend: {}, }, plugins: [], -}; - +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7945f18..30b9850 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,9 +3,11 @@ import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + ], css: { - postcss: './postcss.config.js', + postcss: './postcss.config.mjs', }, server: { host: '0.0.0.0', @@ -45,10 +47,6 @@ export default defineConfig({ target: 'http://backend:3000', changeOrigin: true, }, - '/auth/dev-login': { - target: 'http://backend:3000', - changeOrigin: true, - }, }, }, })