Backup: 2025-06-07 18:32 - Production setup complete
[Restore from backup: vip-coordinator-backup-2025-06-07-18-32-production-setup-complete]
This commit is contained in:
29
.env.prod
Normal file
29
.env.prod
Normal file
@@ -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
|
||||||
30
.env.production
Normal file
30
.env.production
Normal file
@@ -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
|
||||||
173
DOCUMENTATION_CLEANUP_SUMMARY.md
Normal file
173
DOCUMENTATION_CLEANUP_SUMMARY.md
Normal file
@@ -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.
|
||||||
299
README.md
299
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
|
### 🔐 Authentication & User Management
|
||||||
- **Driver Coordination**: Assign and track drivers with real-time location updates
|
- **Google OAuth Integration**: Secure login with Google accounts
|
||||||
- **Flight Tracking**: Monitor flight status and arrival times
|
- **Role-Based Access Control**: Administrator, Coordinator, and Driver roles
|
||||||
- **Schedule Management**: Organize VIP itineraries and driver assignments
|
- **User Approval System**: Admin approval required for new users
|
||||||
- **Real-time Dashboard**: Overview of all active VIPs and available drivers
|
- **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
|
### Backend
|
||||||
- Node.js with Express.js
|
- **Node.js + Express.js**: RESTful API server
|
||||||
- TypeScript for type safety
|
- **TypeScript**: Full type safety
|
||||||
- PostgreSQL database
|
- **PostgreSQL**: Persistent data storage with automatic schema management
|
||||||
- Redis for caching and real-time updates
|
- **Redis**: Caching and real-time updates
|
||||||
- Docker containerization
|
- **JWT Authentication**: Secure, stateless authentication
|
||||||
|
- **Google OAuth 2.0**: Simple, secure user authentication
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
- React 18 with TypeScript
|
- **React 18 + TypeScript**: Modern, type-safe frontend
|
||||||
- Vite for fast development
|
- **Vite**: Lightning-fast development server
|
||||||
- React Router for navigation
|
- **Tailwind CSS v4**: Modern utility-first styling
|
||||||
- Leaflet for mapping (planned)
|
- **React Router**: Client-side routing
|
||||||
- Responsive design with CSS Grid/Flexbox
|
- **Responsive Design**: Mobile-friendly interface
|
||||||
|
|
||||||
## Getting Started
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Docker and Docker Compose
|
- Docker and Docker Compose
|
||||||
- Node.js 18+ (for local development)
|
- Google Cloud Console account (for OAuth setup)
|
||||||
- npm or yarn
|
|
||||||
|
|
||||||
### Quick Start with Docker
|
### 1. Start the Application
|
||||||
|
|
||||||
1. Clone the repository and navigate to the project directory:
|
|
||||||
```bash
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
cd vip-coordinator
|
cd vip-coordinator
|
||||||
```
|
|
||||||
|
|
||||||
2. Start the development environment:
|
|
||||||
```bash
|
|
||||||
make dev
|
make dev
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start all services:
|
**Services will be available at:**
|
||||||
- Frontend: http://localhost:5173
|
- 🌐 **Frontend**: http://localhost:5173
|
||||||
- Backend API: http://localhost:3000
|
- 🔌 **Backend API**: http://localhost:3000
|
||||||
- PostgreSQL: localhost:5432
|
- 📚 **API Documentation**: http://localhost:3000/api-docs.html
|
||||||
- Redis: localhost:6379
|
- 🏥 **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
|
### 3. First Login
|
||||||
```bash
|
- Visit http://localhost:5173
|
||||||
cd backend
|
- Click "Continue with Google"
|
||||||
npm install
|
- First user becomes system administrator
|
||||||
npm run dev
|
- Subsequent users need admin approval
|
||||||
```
|
|
||||||
|
|
||||||
#### Frontend Setup
|
## 📚 API Documentation
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
### Key API Categories
|
||||||
- `GET /api/vips` - List all VIPs
|
- **🔐 Authentication**: `/auth/*` - OAuth, user management, role assignment
|
||||||
- `POST /api/vips` - Create new VIP
|
- **👥 VIPs**: `/api/vips/*` - VIP profiles, scheduling, flight integration
|
||||||
- `GET /api/vips/:id` - Get VIP details
|
- **🚗 Drivers**: `/api/drivers/*` - Driver management, availability, conflicts
|
||||||
- `PUT /api/vips/:id` - Update VIP
|
- **✈️ Flights**: `/api/flights/*` - Flight tracking, real-time updates
|
||||||
- `DELETE /api/vips/:id` - Delete VIP
|
- **⚙️ Admin**: `/api/admin/*` - System settings, user approval
|
||||||
|
|
||||||
### Drivers
|
## 🛠️ Development
|
||||||
- `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
|
|
||||||
|
|
||||||
|
### Available Commands
|
||||||
```bash
|
```bash
|
||||||
# Start development environment
|
# Start development environment
|
||||||
make dev
|
make dev
|
||||||
|
|
||||||
# Build production images
|
# View logs
|
||||||
|
make logs
|
||||||
|
|
||||||
|
# Stop all services
|
||||||
|
make down
|
||||||
|
|
||||||
|
# Rebuild containers
|
||||||
make build
|
make build
|
||||||
|
|
||||||
# Deploy to production
|
# Backend development
|
||||||
make deploy
|
|
||||||
|
|
||||||
# Backend only
|
|
||||||
cd backend && npm run dev
|
cd backend && npm run dev
|
||||||
|
|
||||||
# Frontend only
|
# Frontend development
|
||||||
cd frontend && npm run dev
|
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
|
- [ ] Real-time GPS tracking for drivers
|
||||||
- [ ] Flight API integration for live updates
|
|
||||||
- [ ] Push notifications for schedule changes
|
- [ ] Push notifications for schedule changes
|
||||||
- [ ] Google Sheets import/export
|
- [ ] Mobile driver application
|
||||||
- [ ] Mobile-responsive driver app
|
- [ ] Advanced reporting and analytics
|
||||||
- [ ] Advanced scheduling with drag-and-drop
|
- [ ] Google Sheets integration
|
||||||
- [ ] Reporting and analytics
|
|
||||||
- [ ] Multi-tenant support
|
- [ ] Multi-tenant support
|
||||||
|
- [ ] Advanced mapping features
|
||||||
|
|
||||||
## Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Create a feature branch
|
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
||||||
3. Make your changes
|
3. Make your changes and test thoroughly
|
||||||
4. Test thoroughly
|
4. Commit your changes: `git commit -m 'Add amazing feature'`
|
||||||
5. Submit a pull request
|
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.
|
||||||
|
|||||||
314
SETUP_GUIDE.md
Normal file
314
SETUP_GUIDE.md
Normal file
@@ -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 <repository-url>
|
||||||
|
cd vip-coordinator
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at:
|
||||||
|
- **Frontend**: http://localhost:5173
|
||||||
|
- **Backend API**: http://localhost:3000
|
||||||
|
- **API Documentation**: http://localhost:3000/api-docs.html
|
||||||
|
|
||||||
|
### 2. Google OAuth Setup (Required)
|
||||||
|
|
||||||
|
1. **Create Google Cloud Project**:
|
||||||
|
- Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
- Create a new project or select existing one
|
||||||
|
|
||||||
|
2. **Enable Google+ API**:
|
||||||
|
- Navigate to "APIs & Services" > "Library"
|
||||||
|
- Search for "Google+ API" and enable it
|
||||||
|
|
||||||
|
3. **Create OAuth Credentials**:
|
||||||
|
- Go to "APIs & Services" > "Credentials"
|
||||||
|
- Click "Create Credentials" > "OAuth 2.0 Client IDs"
|
||||||
|
- Application type: "Web application"
|
||||||
|
- Authorized redirect URIs: `http://localhost:3000/auth/google/callback`
|
||||||
|
|
||||||
|
4. **Configure Environment**:
|
||||||
|
```bash
|
||||||
|
# Copy the example environment file
|
||||||
|
cp backend/.env.example backend/.env
|
||||||
|
|
||||||
|
# Edit backend/.env and add your Google OAuth credentials:
|
||||||
|
GOOGLE_CLIENT_ID=your-client-id-here
|
||||||
|
GOOGLE_CLIENT_SECRET=your-client-secret-here
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Restart the Application**:
|
||||||
|
```bash
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. First Login
|
||||||
|
- Visit http://localhost:5173
|
||||||
|
- Click "Continue with Google"
|
||||||
|
- The first user to log in becomes the system administrator
|
||||||
|
- Subsequent users need administrator approval
|
||||||
|
|
||||||
|
## 🏗️ Architecture Overview
|
||||||
|
|
||||||
|
### Authentication System
|
||||||
|
- **JWT-based authentication** with Google OAuth
|
||||||
|
- **Role-based access control**: Administrator, Coordinator, Driver
|
||||||
|
- **User approval system** for new registrations
|
||||||
|
- **Simple setup** - no complex OAuth configurations needed
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- **PostgreSQL** for persistent data storage
|
||||||
|
- **Automatic schema initialization** on first run
|
||||||
|
- **User management** with approval workflows
|
||||||
|
- **VIP and driver data** with scheduling
|
||||||
|
|
||||||
|
### API Structure
|
||||||
|
- **RESTful API** with comprehensive endpoints
|
||||||
|
- **OpenAPI/Swagger documentation** at `/api-docs.html`
|
||||||
|
- **Role-based endpoint protection**
|
||||||
|
- **Real-time flight tracking** integration
|
||||||
|
|
||||||
|
## 📋 Features
|
||||||
|
|
||||||
|
### Current Features
|
||||||
|
- ✅ **User Management**: Google OAuth with role-based access
|
||||||
|
- ✅ **VIP Management**: Create, edit, track VIPs with flight information
|
||||||
|
- ✅ **Driver Coordination**: Manage drivers and assignments
|
||||||
|
- ✅ **Flight Tracking**: Real-time flight status updates
|
||||||
|
- ✅ **Schedule Management**: Event scheduling with conflict detection
|
||||||
|
- ✅ **Department Support**: Office of Development and Admin departments
|
||||||
|
- ✅ **API Documentation**: Interactive Swagger UI
|
||||||
|
|
||||||
|
### User Roles
|
||||||
|
- **Administrator**: Full system access, user management
|
||||||
|
- **Coordinator**: VIP and driver management, scheduling
|
||||||
|
- **Driver**: View assigned schedules (planned)
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
```bash
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql://vip_user:vip_password@db:5432/vip_coordinator
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
GOOGLE_CLIENT_ID=your-google-client-id
|
||||||
|
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||||
|
JWT_SECRET=your-jwt-secret-key
|
||||||
|
|
||||||
|
# External APIs (Optional)
|
||||||
|
AVIATIONSTACK_API_KEY=your-aviationstack-key
|
||||||
|
|
||||||
|
# Application
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Services
|
||||||
|
- **Frontend**: React + Vite development server
|
||||||
|
- **Backend**: Node.js + Express API server
|
||||||
|
- **Database**: PostgreSQL with automatic initialization
|
||||||
|
- **Redis**: Caching and real-time updates
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
### Available Commands
|
||||||
|
```bash
|
||||||
|
# Start development environment
|
||||||
|
make dev
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
make logs
|
||||||
|
|
||||||
|
# Stop all services
|
||||||
|
make down
|
||||||
|
|
||||||
|
# Rebuild containers
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Backend only
|
||||||
|
cd backend && npm run dev
|
||||||
|
|
||||||
|
# Frontend only
|
||||||
|
cd frontend && npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Testing
|
||||||
|
- **Interactive Documentation**: http://localhost:3000/api-docs.html
|
||||||
|
- **Health Check**: http://localhost:3000/api/health
|
||||||
|
- **Authentication Test**: Use the "Try it out" feature in Swagger UI
|
||||||
|
|
||||||
|
## 🔐 Security
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
1. User clicks "Continue with Google"
|
||||||
|
2. Redirected to Google OAuth
|
||||||
|
3. Google redirects back with authorization code
|
||||||
|
4. Backend exchanges code for user info
|
||||||
|
5. JWT token generated and returned
|
||||||
|
6. Frontend stores token for API requests
|
||||||
|
|
||||||
|
### API Protection
|
||||||
|
- All API endpoints require valid JWT token
|
||||||
|
- Role-based access control on sensitive operations
|
||||||
|
- User approval system for new registrations
|
||||||
|
|
||||||
|
## 📚 API Documentation
|
||||||
|
|
||||||
|
### Key Endpoints
|
||||||
|
- **Authentication**: `/auth/*` - OAuth and user management
|
||||||
|
- **VIPs**: `/api/vips/*` - VIP management and scheduling
|
||||||
|
- **Drivers**: `/api/drivers/*` - Driver management and availability
|
||||||
|
- **Flights**: `/api/flights/*` - Flight tracking and information
|
||||||
|
- **Admin**: `/api/admin/*` - System administration
|
||||||
|
|
||||||
|
### Interactive Documentation
|
||||||
|
Visit http://localhost:3000/api-docs.html for:
|
||||||
|
- Complete API reference
|
||||||
|
- Request/response examples
|
||||||
|
- "Try it out" functionality
|
||||||
|
- Schema definitions
|
||||||
|
|
||||||
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**OAuth Not Working**:
|
||||||
|
- Verify Google Client ID and Secret in `.env`
|
||||||
|
- Check redirect URI in Google Console matches exactly
|
||||||
|
- Ensure Google+ API is enabled
|
||||||
|
|
||||||
|
**Database Connection Error**:
|
||||||
|
- Verify Docker containers are running: `docker ps`
|
||||||
|
- Check database logs: `docker-compose logs db`
|
||||||
|
- Restart services: `make down && make dev`
|
||||||
|
|
||||||
|
**Frontend Can't Connect to Backend**:
|
||||||
|
- Verify backend is running on port 3000
|
||||||
|
- Check CORS configuration in backend
|
||||||
|
- Ensure FRONTEND_URL is set correctly
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
1. Check the interactive API documentation
|
||||||
|
2. Review Docker container logs
|
||||||
|
3. Verify environment configuration
|
||||||
|
4. Test with the health check endpoint
|
||||||
|
|
||||||
|
## 🔄 Production Deployment
|
||||||
|
|
||||||
|
### Prerequisites for Production
|
||||||
|
|
||||||
|
1. **Domain Setup**: Ensure your domains are configured:
|
||||||
|
- Frontend: `https://bsa.madeamess.online`
|
||||||
|
- API: `https://api.bsa.madeamess.online`
|
||||||
|
|
||||||
|
2. **SSL Certificates**: Configure SSL/TLS certificates for your domains
|
||||||
|
|
||||||
|
3. **Environment Configuration**: Copy and configure production environment:
|
||||||
|
```bash
|
||||||
|
cp .env.production .env.prod
|
||||||
|
# Edit .env.prod with your secure values
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Deployment Steps
|
||||||
|
|
||||||
|
1. **Configure Environment Variables**:
|
||||||
|
```bash
|
||||||
|
# Edit .env.prod with secure values:
|
||||||
|
# - Change DB_PASSWORD to a strong password
|
||||||
|
# - Generate new JWT_SECRET and SESSION_SECRET
|
||||||
|
# - Update ADMIN_PASSWORD
|
||||||
|
# - Set your AVIATIONSTACK_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploy with Production Configuration**:
|
||||||
|
```bash
|
||||||
|
# Load production environment
|
||||||
|
export $(cat .env.prod | xargs)
|
||||||
|
|
||||||
|
# Build and start production containers
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify Deployment**:
|
||||||
|
```bash
|
||||||
|
# Check container status
|
||||||
|
docker-compose -f docker-compose.prod.yml ps
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose -f docker-compose.prod.yml logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production vs Development Differences
|
||||||
|
|
||||||
|
| Feature | Development | Production |
|
||||||
|
|---------|-------------|------------|
|
||||||
|
| Build Target | `development` | `production` |
|
||||||
|
| Source Code | Volume mounted (hot reload) | Built into image |
|
||||||
|
| Database Password | Hardcoded `changeme` | Environment variable |
|
||||||
|
| Frontend Server | Vite dev server (port 5173) | Nginx (port 80) |
|
||||||
|
| API URL | `http://localhost:3000/api` | `https://api.bsa.madeamess.online/api` |
|
||||||
|
| SSL/HTTPS | Not configured | Required |
|
||||||
|
| Restart Policy | Manual | `unless-stopped` |
|
||||||
|
|
||||||
|
### Production Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database Configuration
|
||||||
|
DB_PASSWORD=your-secure-database-password-here
|
||||||
|
|
||||||
|
# Domain Configuration
|
||||||
|
DOMAIN=bsa.madeamess.online
|
||||||
|
VITE_API_URL=https://api.bsa.madeamess.online/api
|
||||||
|
|
||||||
|
# Authentication Configuration (Generate new secure keys)
|
||||||
|
JWT_SECRET=your-super-secure-jwt-secret-key-change-in-production-12345
|
||||||
|
SESSION_SECRET=your-super-secure-session-secret-change-in-production-67890
|
||||||
|
|
||||||
|
# Google OAuth Configuration
|
||||||
|
GOOGLE_CLIENT_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
|
||||||
|
GOOGLE_CLIENT_SECRET=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
|
||||||
|
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
|
||||||
|
|
||||||
|
# Frontend URL
|
||||||
|
FRONTEND_URL=https://bsa.madeamess.online
|
||||||
|
|
||||||
|
# Flight API Configuration
|
||||||
|
AVIATIONSTACK_API_KEY=your-aviationstack-api-key
|
||||||
|
|
||||||
|
# Admin Configuration
|
||||||
|
ADMIN_PASSWORD=your-secure-admin-password
|
||||||
|
|
||||||
|
# Port Configuration
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production-Specific Troubleshooting
|
||||||
|
|
||||||
|
**SSL Certificate errors**: Ensure certificates are properly configured
|
||||||
|
**Domain resolution**: Verify DNS settings for your domains
|
||||||
|
**Environment variables**: Check that all required variables are set in `.env.prod`
|
||||||
|
**Firewall**: Ensure ports 80, 443, 3000 are accessible
|
||||||
|
|
||||||
|
### Production Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View production container logs
|
||||||
|
docker-compose -f docker-compose.prod.yml logs backend
|
||||||
|
docker-compose -f docker-compose.prod.yml logs frontend
|
||||||
|
docker-compose -f docker-compose.prod.yml logs db
|
||||||
|
|
||||||
|
# Follow logs in real-time
|
||||||
|
docker-compose -f docker-compose.prod.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
This setup guide reflects the current simple, effective architecture of the VIP Coordinator system with production-ready deployment capabilities.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Database Configuration
|
# Database Configuration
|
||||||
DATABASE_URL=postgresql://postgres:password@db:5432/vip_coordinator
|
DATABASE_URL=postgresql://postgres:changeme@db:5432/vip_coordinator
|
||||||
|
|
||||||
# Redis Configuration
|
# Redis Configuration
|
||||||
REDIS_URL=redis://redis:6379
|
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
|
JWT_SECRET=your-super-secure-jwt-secret-key-change-in-production-12345
|
||||||
SESSION_SECRET=your-super-secure-session-secret-change-in-production-67890
|
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_ID=308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com
|
||||||
GOOGLE_CLIENT_SECRET=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
|
GOOGLE_CLIENT_SECRET=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
|
||||||
GOOGLE_REDIRECT_URI=https://api.bsa.madeamess.online/auth/google/callback
|
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 Configuration
|
||||||
ADMIN_PASSWORD=admin123
|
ADMIN_PASSWORD=admin123
|
||||||
|
|
||||||
|
# Port Configuration
|
||||||
|
PORT=3000
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Multi-stage build for development and production
|
# Multi-stage build for development and production
|
||||||
FROM node:18-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -15,10 +15,7 @@ CMD ["npm", "run", "dev"]
|
|||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
RUN npm ci
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
|
||||||
RUN npm prune --omit=dev
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "run", "dev"]
|
||||||
|
|||||||
203
backend/package-lock.json
generated
203
backend/package-lock.json
generated
@@ -13,7 +13,6 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwks-rsa": "^3.2.0",
|
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"redis": "^4.6.8",
|
"redis": "^4.6.8",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
@@ -80,7 +79,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
|
||||||
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
|
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cluster-key-slot": "1.1.2",
|
"cluster-key-slot": "1.1.2",
|
||||||
"generic-pool": "3.9.0",
|
"generic-pool": "3.9.0",
|
||||||
@@ -154,6 +152,7 @@
|
|||||||
"version": "1.19.5",
|
"version": "1.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/connect": "*",
|
"@types/connect": "*",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -163,6 +162,7 @@
|
|||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@@ -180,6 +180,7 @@
|
|||||||
"version": "4.17.22",
|
"version": "4.17.22",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz",
|
||||||
"integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==",
|
"integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/body-parser": "*",
|
"@types/body-parser": "*",
|
||||||
"@types/express-serve-static-core": "^4.17.33",
|
"@types/express-serve-static-core": "^4.17.33",
|
||||||
@@ -191,6 +192,7 @@
|
|||||||
"version": "4.19.6",
|
"version": "4.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
||||||
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/qs": "*",
|
"@types/qs": "*",
|
||||||
@@ -201,12 +203,14 @@
|
|||||||
"node_modules/@types/http-errors": {
|
"node_modules/@types/http-errors": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
"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": {
|
"node_modules/@types/jsonwebtoken": {
|
||||||
"version": "9.0.9",
|
"version": "9.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
|
||||||
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
|
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/ms": "*",
|
"@types/ms": "*",
|
||||||
@@ -216,19 +220,21 @@
|
|||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"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": {
|
"node_modules/@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"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,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.17.57",
|
"version": "20.17.57",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
|
||||||
"integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==",
|
"integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==",
|
||||||
"peer": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
@@ -248,17 +254,20 @@
|
|||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"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": {
|
"node_modules/@types/range-parser": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
"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": {
|
"node_modules/@types/send": {
|
||||||
"version": "0.17.4",
|
"version": "0.17.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/mime": "^1",
|
"@types/mime": "^1",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -268,6 +277,7 @@
|
|||||||
"version": "1.15.7",
|
"version": "1.15.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
||||||
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/http-errors": "*",
|
"@types/http-errors": "*",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
@@ -1016,15 +1026,6 @@
|
|||||||
"node": ">=0.12.0"
|
"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": {
|
"node_modules/jsonwebtoken": {
|
||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
@@ -1064,46 +1065,6 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"node_modules/jws": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||||
@@ -1114,17 +1075,6 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@@ -1167,28 +1117,6 @@
|
|||||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz",
|
||||||
"integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==",
|
"integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.9.0",
|
"pg-connection-string": "^2.9.0",
|
||||||
"pg-pool": "^3.10.0",
|
"pg-pool": "^3.10.0",
|
||||||
@@ -1990,7 +1917,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -2002,7 +1928,8 @@
|
|||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"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": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -2119,7 +2046,6 @@
|
|||||||
"version": "1.6.1",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
|
||||||
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
|
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"cluster-key-slot": "1.1.2",
|
"cluster-key-slot": "1.1.2",
|
||||||
"generic-pool": "3.9.0",
|
"generic-pool": "3.9.0",
|
||||||
@@ -2178,6 +2104,7 @@
|
|||||||
"version": "1.19.5",
|
"version": "1.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/connect": "*",
|
"@types/connect": "*",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -2187,6 +2114,7 @@
|
|||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@@ -2204,6 +2132,7 @@
|
|||||||
"version": "4.17.22",
|
"version": "4.17.22",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz",
|
||||||
"integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==",
|
"integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/body-parser": "*",
|
"@types/body-parser": "*",
|
||||||
"@types/express-serve-static-core": "^4.17.33",
|
"@types/express-serve-static-core": "^4.17.33",
|
||||||
@@ -2215,6 +2144,7 @@
|
|||||||
"version": "4.19.6",
|
"version": "4.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
||||||
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/qs": "*",
|
"@types/qs": "*",
|
||||||
@@ -2225,12 +2155,14 @@
|
|||||||
"@types/http-errors": {
|
"@types/http-errors": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
"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": {
|
"@types/jsonwebtoken": {
|
||||||
"version": "9.0.9",
|
"version": "9.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
|
||||||
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
|
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/ms": "*",
|
"@types/ms": "*",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -2239,18 +2171,20 @@
|
|||||||
"@types/mime": {
|
"@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"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": {
|
"@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"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": {
|
"@types/node": {
|
||||||
"version": "20.17.57",
|
"version": "20.17.57",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
|
||||||
"integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==",
|
"integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==",
|
||||||
"peer": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
@@ -2269,17 +2203,20 @@
|
|||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"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": {
|
"@types/range-parser": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
"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": {
|
"@types/send": {
|
||||||
"version": "0.17.4",
|
"version": "0.17.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/mime": "^1",
|
"@types/mime": "^1",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -2289,6 +2226,7 @@
|
|||||||
"version": "1.15.7",
|
"version": "1.15.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
||||||
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/http-errors": "*",
|
"@types/http-errors": "*",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
@@ -2841,11 +2779,6 @@
|
|||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
"dev": true
|
"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": {
|
"jsonwebtoken": {
|
||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
@@ -2880,34 +2813,6 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"jws": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||||
@@ -2917,16 +2822,6 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"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": {
|
"lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
"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": {
|
"make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
@@ -3113,7 +2991,6 @@
|
|||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz",
|
||||||
"integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==",
|
"integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==",
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"pg-cloudflare": "^1.2.5",
|
"pg-cloudflare": "^1.2.5",
|
||||||
"pg-connection-string": "^2.9.0",
|
"pg-connection-string": "^2.9.0",
|
||||||
@@ -3509,13 +3386,13 @@
|
|||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"undici-types": {
|
"undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"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": {
|
"unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
"dev": "npx tsx src/index.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwks-rsa": "^3.2.0",
|
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"redis": "^4.6.8",
|
"redis": "^4.6.8",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
@@ -34,7 +33,9 @@
|
|||||||
"@types/node": "^20.5.0",
|
"@types/node": "^20.5.0",
|
||||||
"@types/pg": "^8.10.2",
|
"@types/pg": "^8.10.2",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.2",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "^5.1.6"
|
"tsx": "^4.7.0",
|
||||||
|
"typescript": "^5.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ import dotenv from 'dotenv';
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const useSSL = process.env.DATABASE_SSL === 'true';
|
|
||||||
|
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:changeme@localhost:5432/vip_coordinator',
|
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:changeme@localhost:5432/vip_coordinator',
|
||||||
ssl: useSSL ? { rejectUnauthorized: false } : false,
|
|
||||||
max: 20,
|
max: 20,
|
||||||
idleTimeoutMillis: 30000,
|
idleTimeoutMillis: 30000,
|
||||||
connectionTimeoutMillis: 2000,
|
connectionTimeoutMillis: 2000,
|
||||||
|
|||||||
@@ -1,178 +1,134 @@
|
|||||||
import jwt, { JwtHeader, JwtPayload } from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import jwksClient from 'jwks-rsa';
|
|
||||||
|
|
||||||
const auth0Domain = process.env.AUTH0_DOMAIN;
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||||||
const auth0Audience = process.env.AUTH0_AUDIENCE;
|
|
||||||
|
|
||||||
if (!auth0Domain) {
|
export interface User {
|
||||||
console.warn('⚠️ AUTH0_DOMAIN is not set. Authentication routes will reject requests until configured.');
|
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) {
|
export function generateToken(user: User): string {
|
||||||
console.warn('⚠️ AUTH0_AUDIENCE is not set. Authentication routes will reject requests until configured.');
|
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
|
export function verifyToken(token: string): User | null {
|
||||||
? jwksClient({
|
try {
|
||||||
jwksUri: `https://${auth0Domain}/.well-known/jwks.json`,
|
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||||
cache: true,
|
return {
|
||||||
cacheMaxEntries: 5,
|
id: decoded.id,
|
||||||
cacheMaxAge: 10 * 60 * 1000
|
google_id: decoded.google_id,
|
||||||
})
|
email: decoded.email,
|
||||||
: null;
|
name: decoded.name,
|
||||||
|
profile_picture_url: decoded.profile_picture_url,
|
||||||
const PROFILE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
role: decoded.role
|
||||||
const profileCache = new Map<string, { profile: Auth0UserProfile; expiresAt: number }>();
|
};
|
||||||
const inflightProfileRequests = new Map<string, Promise<Auth0UserProfile>>();
|
} catch (error) {
|
||||||
|
return null;
|
||||||
export interface Auth0UserProfile {
|
}
|
||||||
sub: string;
|
|
||||||
email?: string;
|
|
||||||
name?: string;
|
|
||||||
nickname?: string;
|
|
||||||
picture?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifiedAccessToken extends JwtPayload {
|
// Simple Google OAuth2 client using fetch
|
||||||
sub: string;
|
export async function verifyGoogleToken(googleToken: string): Promise<any> {
|
||||||
azp?: string;
|
try {
|
||||||
scope?: string;
|
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<string> {
|
// Get Google OAuth2 URL
|
||||||
if (!jwks) {
|
export function getGoogleAuthUrl(): string {
|
||||||
throw new Error('Auth0 JWKS client not initialised');
|
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) {
|
const params = new URLSearchParams({
|
||||||
throw new Error('Token signing key id (kid) is missing');
|
client_id: clientId,
|
||||||
}
|
redirect_uri: redirectUri,
|
||||||
|
response_type: 'code',
|
||||||
const signingKey = await new Promise<jwksClient.SigningKey>((resolve, reject) => {
|
scope: 'openid email profile',
|
||||||
jwks.getSigningKey(header.kid as string, (err, key) => {
|
access_type: 'offline',
|
||||||
if (err) {
|
prompt: 'consent'
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
if (!key) {
|
|
||||||
return reject(new Error('Signing key not found'));
|
|
||||||
}
|
|
||||||
resolve(key);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const publicKey =
|
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyAccessToken(token: string): Promise<VerifiedAccessToken> {
|
// Exchange authorization code for tokens
|
||||||
if (!auth0Domain || !auth0Audience) {
|
export async function exchangeCodeForTokens(code: string): Promise<any> {
|
||||||
throw new Error('Auth0 configuration is incomplete');
|
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 });
|
try {
|
||||||
if (!decoded || typeof decoded === 'string') {
|
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||||
throw new Error('Invalid JWT');
|
method: 'POST',
|
||||||
}
|
|
||||||
|
|
||||||
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<Auth0UserProfile> {
|
|
||||||
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`, {
|
|
||||||
headers: {
|
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) {
|
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;
|
return await response.json();
|
||||||
profileCache.set(cacheKey, { profile, expiresAt: now + ttl });
|
} catch (error) {
|
||||||
inflightProfileRequests.delete(cacheKey);
|
console.error('Error exchanging code for tokens:', error);
|
||||||
return profile;
|
|
||||||
})().catch(error => {
|
|
||||||
inflightProfileRequests.delete(cacheKey);
|
|
||||||
throw 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 {
|
// Get user info from Google
|
||||||
const cached = profileCache.get(cacheKey);
|
export async function getGoogleUserInfo(accessToken: string): Promise<any> {
|
||||||
if (cached && cached.expiresAt > Date.now()) {
|
try {
|
||||||
return cached.profile;
|
const response = await fetch(`https://www.googleapis.com/oauth2/v2/userinfo?access_token=${accessToken}`);
|
||||||
}
|
|
||||||
return undefined;
|
if (!response.ok) {
|
||||||
}
|
throw new Error('Failed to get user info');
|
||||||
|
|
||||||
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<Auth0UserProfile> {
|
|
||||||
if (!auth0Domain) {
|
|
||||||
throw new Error('Auth0 configuration is incomplete');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`https://${auth0Domain}/userinfo`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
return await response.json();
|
||||||
throw new Error(`Failed to fetch Auth0 user profile (${response.status})`);
|
} 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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ app.use(cors({
|
|||||||
origin: [
|
origin: [
|
||||||
process.env.FRONTEND_URL || 'http://localhost:5173',
|
process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||||
'https://bsa.madeamess.online:5173',
|
'https://bsa.madeamess.online:5173',
|
||||||
|
'https://bsa.madeamess.online',
|
||||||
|
'https://api.bsa.madeamess.online',
|
||||||
'http://bsa.madeamess.online:5173'
|
'http://bsa.madeamess.online:5173'
|
||||||
],
|
],
|
||||||
credentials: true
|
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!
|
// 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
|
// Initialize flight tracking scheduler
|
||||||
const flightTracker = new FlightTrackingScheduler(flightService);
|
const flightTracker = new FlightTrackingScheduler(flightService);
|
||||||
|
|
||||||
@@ -606,21 +611,35 @@ app.get('/api/drivers/:driverId/schedule', requireAuth, async (req: Request, res
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Admin routes
|
// 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 {
|
try {
|
||||||
const adminSettings = await enhancedDataService.getAdminSettings();
|
const adminSettings = await enhancedDataService.getAdminSettings();
|
||||||
const apiKeys = adminSettings.apiKeys || {};
|
|
||||||
|
|
||||||
// Return settings but mask API keys for display only
|
// Return settings but mask API keys for display only
|
||||||
// IMPORTANT: Don't return the actual keys, just indicate they exist
|
// IMPORTANT: Don't return the actual keys, just indicate they exist
|
||||||
const maskedSettings = {
|
const maskedSettings = {
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
aviationStackKey: apiKeys.aviationStackKey ? '***' + apiKeys.aviationStackKey.slice(-4) : '',
|
aviationStackKey: adminSettings.apiKeys.aviationStackKey ? '***' + adminSettings.apiKeys.aviationStackKey.slice(-4) : '',
|
||||||
googleMapsKey: apiKeys.googleMapsKey ? '***' + apiKeys.googleMapsKey.slice(-4) : '',
|
googleMapsKey: adminSettings.apiKeys.googleMapsKey ? '***' + adminSettings.apiKeys.googleMapsKey.slice(-4) : '',
|
||||||
twilioKey: apiKeys.twilioKey ? '***' + apiKeys.twilioKey.slice(-4) : '',
|
twilioKey: adminSettings.apiKeys.twilioKey ? '***' + adminSettings.apiKeys.twilioKey.slice(-4) : '',
|
||||||
auth0Domain: apiKeys.auth0Domain ? '***' + apiKeys.auth0Domain.slice(-4) : '',
|
googleClientId: adminSettings.apiKeys.googleClientId ? '***' + adminSettings.apiKeys.googleClientId.slice(-4) : '',
|
||||||
auth0ClientId: apiKeys.auth0ClientId ? '***' + apiKeys.auth0ClientId.slice(-4) : '',
|
googleClientSecret: adminSettings.apiKeys.googleClientSecret ? '***' + adminSettings.apiKeys.googleClientSecret.slice(-4) : ''
|
||||||
auth0ClientSecret: apiKeys.auth0ClientSecret ? '***' + apiKeys.auth0ClientSecret.slice(-4) : ''
|
|
||||||
},
|
},
|
||||||
systemSettings: adminSettings.systemSettings
|
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 {
|
try {
|
||||||
const { apiKeys, systemSettings } = req.body;
|
const { apiKeys, systemSettings } = req.body;
|
||||||
const currentSettings = await enhancedDataService.getAdminSettings();
|
const currentSettings = await enhancedDataService.getAdminSettings();
|
||||||
currentSettings.apiKeys = currentSettings.apiKeys || {};
|
|
||||||
|
|
||||||
// Update API keys (only if provided and not masked)
|
// Update API keys (only if provided and not masked)
|
||||||
if (apiKeys) {
|
if (apiKeys) {
|
||||||
@@ -650,17 +674,15 @@ app.post('/api/admin/settings', requireAuth, requireRole(['administrator']), asy
|
|||||||
if (apiKeys.twilioKey && !apiKeys.twilioKey.startsWith('***')) {
|
if (apiKeys.twilioKey && !apiKeys.twilioKey.startsWith('***')) {
|
||||||
currentSettings.apiKeys.twilioKey = apiKeys.twilioKey;
|
currentSettings.apiKeys.twilioKey = apiKeys.twilioKey;
|
||||||
}
|
}
|
||||||
if (apiKeys.auth0Domain && !apiKeys.auth0Domain.startsWith('***')) {
|
if (apiKeys.googleClientId && !apiKeys.googleClientId.startsWith('***')) {
|
||||||
currentSettings.apiKeys.auth0Domain = apiKeys.auth0Domain;
|
currentSettings.apiKeys.googleClientId = apiKeys.googleClientId;
|
||||||
process.env.AUTH0_DOMAIN = apiKeys.auth0Domain;
|
// Update the environment variable for Google OAuth
|
||||||
|
process.env.GOOGLE_CLIENT_ID = apiKeys.googleClientId;
|
||||||
}
|
}
|
||||||
if (apiKeys.auth0ClientId && !apiKeys.auth0ClientId.startsWith('***')) {
|
if (apiKeys.googleClientSecret && !apiKeys.googleClientSecret.startsWith('***')) {
|
||||||
currentSettings.apiKeys.auth0ClientId = apiKeys.auth0ClientId;
|
currentSettings.apiKeys.googleClientSecret = apiKeys.googleClientSecret;
|
||||||
process.env.AUTH0_CLIENT_ID = apiKeys.auth0ClientId;
|
// Update the environment variable for Google OAuth
|
||||||
}
|
process.env.GOOGLE_CLIENT_SECRET = apiKeys.googleClientSecret;
|
||||||
if (apiKeys.auth0ClientSecret && !apiKeys.auth0ClientSecret.startsWith('***')) {
|
|
||||||
currentSettings.apiKeys.auth0ClientSecret = apiKeys.auth0ClientSecret;
|
|
||||||
process.env.AUTH0_CLIENT_SECRET = apiKeys.auth0ClientSecret;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 { apiType } = req.params;
|
||||||
const { apiKey } = req.body;
|
const { apiKey } = req.body;
|
||||||
|
|
||||||
@@ -727,6 +755,7 @@ async function startServer() {
|
|||||||
// Start the server
|
// Start the server
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`🚀 Server is running on port ${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(`📊 Admin dashboard: http://localhost:${port === 3000 ? 5173 : port}/admin`);
|
||||||
console.log(`🏥 Health check: http://localhost:${port}/api/health`);
|
console.log(`🏥 Health check: http://localhost:${port}/api/health`);
|
||||||
console.log(`📚 API docs: http://localhost:${port}/api-docs.html`);
|
console.log(`📚 API docs: http://localhost:${port}/api-docs.html`);
|
||||||
|
|||||||
@@ -1,176 +1,64 @@
|
|||||||
import express, { Request, Response, NextFunction } from 'express';
|
import express, { Request, Response, NextFunction } from 'express';
|
||||||
import {
|
import {
|
||||||
fetchAuth0UserProfile,
|
generateToken,
|
||||||
isAuth0Configured,
|
verifyToken,
|
||||||
verifyAccessToken,
|
getGoogleAuthUrl,
|
||||||
VerifiedAccessToken,
|
exchangeCodeForTokens,
|
||||||
Auth0UserProfile,
|
getGoogleUserInfo,
|
||||||
getCachedProfile,
|
User
|
||||||
cacheAuth0Profile
|
|
||||||
} from '../config/simpleAuth';
|
} from '../config/simpleAuth';
|
||||||
import databaseService from '../services/databaseService';
|
import databaseService from '../services/databaseService';
|
||||||
|
|
||||||
type AuthedRequest = Request & {
|
|
||||||
auth?: {
|
|
||||||
token: string;
|
|
||||||
claims: VerifiedAccessToken;
|
|
||||||
profile?: Auth0UserProfile | null;
|
|
||||||
};
|
|
||||||
user?: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
function mapUserForResponse(user: any) {
|
// Middleware to check authentication
|
||||||
return {
|
export function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||||
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) {
|
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
return res.status(401).json({ error: 'No token provided' });
|
return res.status(401).json({ error: 'No token provided' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = authHeader.substring(7);
|
const token = authHeader.substring(7);
|
||||||
|
const user = verifyToken(token);
|
||||||
|
|
||||||
try {
|
if (!user) {
|
||||||
const claims = await verifyAccessToken(token);
|
return res.status(401).json({ error: 'Invalid 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' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(req as any).user = user;
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Middleware to check role
|
||||||
export function requireRole(roles: string[]) {
|
export function requireRole(roles: string[]) {
|
||||||
return (req: AuthedRequest, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
const user = req.user;
|
const user = (req as any).user;
|
||||||
|
|
||||||
if (!user || !roles.includes(user.role)) {
|
if (!user || !roles.includes(user.role)) {
|
||||||
return res.status(403).json({ error: 'Insufficient permissions' });
|
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
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 {
|
try {
|
||||||
const userCount = await databaseService.getUserCount();
|
const userCount = await databaseService.getUserCount();
|
||||||
res.json({
|
res.json({
|
||||||
setupCompleted: isAuth0Configured(),
|
setupCompleted: !!(clientId && clientSecret && clientId !== 'your-google-client-id-from-console'),
|
||||||
firstAdminCreated: userCount > 0,
|
firstAdminCreated: userCount > 0,
|
||||||
oauthConfigured: isAuth0Configured(),
|
oauthConfigured: !!(clientId && clientSecret)
|
||||||
authProvider: 'auth0'
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking setup status:', 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) => {
|
// Start Google OAuth flow
|
||||||
res.json({
|
router.get('/google', (req: Request, res: Response) => {
|
||||||
user: mapUserForResponse(req.user),
|
try {
|
||||||
auth0: {
|
const authUrl = getGoogleAuthUrl();
|
||||||
sub: req.auth?.claims.sub,
|
res.redirect(authUrl);
|
||||||
scope: req.auth?.claims.scope
|
} 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' });
|
res.json({ message: 'Logged out successfully' });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/status', requireAuth, (req: AuthedRequest, res: Response) => {
|
// Get auth status
|
||||||
res.json({
|
router.get('/status', (req: Request, res: Response) => {
|
||||||
authenticated: true,
|
const authHeader = req.headers.authorization;
|
||||||
user: mapUserForResponse(req.user)
|
|
||||||
|
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
|
// USER MANAGEMENT ENDPOINTS
|
||||||
|
|
||||||
// List all users (admin only)
|
// 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 {
|
try {
|
||||||
const users = await databaseService.getAllUsers();
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching users:', error);
|
console.error('Error fetching users:', error);
|
||||||
res.status(500).json({ error: 'Failed to fetch users' });
|
res.status(500).json({ error: 'Failed to fetch users' });
|
||||||
@@ -230,7 +289,12 @@ router.patch('/users/:email/role', requireAuth, requireRole(['administrator']),
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
user: mapUserForResponse(user)
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: user.role
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating user role:', error);
|
console.error('Error updating user role:', error);
|
||||||
@@ -239,9 +303,9 @@ router.patch('/users/:email/role', requireAuth, requireRole(['administrator']),
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Delete user (admin only)
|
// 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 { email } = req.params;
|
||||||
const currentUser = req.user;
|
const currentUser = (req as any).user;
|
||||||
|
|
||||||
// Prevent admin from deleting themselves
|
// Prevent admin from deleting themselves
|
||||||
if (email === currentUser.email) {
|
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' });
|
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) {
|
} catch (error) {
|
||||||
console.error('Error fetching user:', error);
|
console.error('Error fetching user:', error);
|
||||||
res.status(500).json({ error: 'Failed to fetch user' });
|
res.status(500).json({ error: 'Failed to fetch user' });
|
||||||
@@ -286,7 +360,16 @@ router.get('/users/pending/list', requireAuth, requireRole(['administrator']), a
|
|||||||
try {
|
try {
|
||||||
const pendingUsers = await databaseService.getPendingUsers();
|
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);
|
res.json(userList);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -313,7 +396,13 @@ router.patch('/users/:email/approval', requireAuth, requireRole(['administrator'
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `User ${status} successfully`,
|
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) {
|
} catch (error) {
|
||||||
console.error('Error updating user approval:', error);
|
console.error('Error updating user approval:', error);
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ class DatabaseService {
|
|||||||
private redis: RedisClientType;
|
private redis: RedisClientType;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const useSSL = process.env.DATABASE_SSL === 'true';
|
|
||||||
this.pool = new Pool({
|
this.pool = new Pool({
|
||||||
connectionString: process.env.DATABASE_URL,
|
connectionString: process.env.DATABASE_URL,
|
||||||
ssl: useSSL ? { rejectUnauthorized: false } : false
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize Redis connection
|
// 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'))
|
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
|
// Create indexes
|
||||||
await this.query(`
|
await this.query(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id)
|
CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id)
|
||||||
@@ -191,31 +158,6 @@ class DatabaseService {
|
|||||||
return result.rows[0] || null;
|
return result.rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrateUserId(oldId: string, newId: string): Promise<void> {
|
|
||||||
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<any[]> {
|
async getAllUsers(): Promise<any[]> {
|
||||||
const query = 'SELECT * FROM users ORDER BY created_at ASC';
|
const query = 'SELECT * FROM users ORDER BY created_at ASC';
|
||||||
const result = await this.query(query);
|
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<void> {
|
async initializeVipTables(): Promise<void> {
|
||||||
try {
|
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(`
|
await this.query(`
|
||||||
CREATE TABLE IF NOT EXISTS vips (
|
CREATE TABLE IF NOT EXISTS vips (
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
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,
|
notes TEXT,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await this.query(`
|
// Create flights table (for VIPs with flight transport)
|
||||||
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
|
|
||||||
`);
|
|
||||||
|
|
||||||
await this.query(`
|
await this.query(`
|
||||||
CREATE TABLE IF NOT EXISTS flights (
|
CREATE TABLE IF NOT EXISTS flights (
|
||||||
id SERIAL PRIMARY KEY,
|
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(`
|
await this.query(`
|
||||||
CREATE TABLE IF NOT EXISTS drivers (
|
CREATE TABLE IF NOT EXISTS drivers (
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
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,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await this.query(`
|
// Check and migrate schedule_events table
|
||||||
ALTER TABLE drivers
|
const scheduleTableExists = await this.query(`
|
||||||
ADD COLUMN IF NOT EXISTS phone VARCHAR(50),
|
SELECT EXISTS (
|
||||||
ADD COLUMN IF NOT EXISTS department VARCHAR(255) DEFAULT 'Office of Development',
|
SELECT FROM information_schema.tables
|
||||||
ADD COLUMN IF NOT EXISTS user_id VARCHAR(255) REFERENCES users(id) ON DELETE SET NULL
|
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(`
|
await this.query(`
|
||||||
CREATE TABLE IF NOT EXISTS schedule_events (
|
CREATE TABLE IF NOT EXISTS schedule_events (
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
@@ -388,42 +400,66 @@ class DatabaseService {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Create system_setup table for tracking initial setup
|
||||||
await this.query(`
|
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(`
|
await this.query(`
|
||||||
ALTER TABLE schedule_events
|
CREATE TABLE IF NOT EXISTS admin_settings (
|
||||||
ADD COLUMN IF NOT EXISTS description TEXT,
|
id SERIAL PRIMARY KEY,
|
||||||
ADD COLUMN IF NOT EXISTS assigned_driver_id VARCHAR(255) REFERENCES drivers(id) ON DELETE SET NULL,
|
setting_key VARCHAR(255) UNIQUE NOT NULL,
|
||||||
ADD COLUMN IF NOT EXISTS status VARCHAR(50) DEFAULT 'scheduled',
|
setting_value TEXT,
|
||||||
ADD COLUMN IF NOT EXISTS event_type VARCHAR(50),
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
ADD COLUMN IF NOT EXISTS 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(`
|
await this.query(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_vips_transport_mode ON vips(transport_mode)
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
`);
|
RETURNS TRIGGER AS $$
|
||||||
await this.query(`
|
BEGIN
|
||||||
CREATE INDEX IF NOT EXISTS idx_flights_vip_id ON flights(vip_id)
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
`);
|
RETURN NEW;
|
||||||
await this.query(`
|
END;
|
||||||
CREATE INDEX IF NOT EXISTS idx_flights_date ON flights(flight_date)
|
$$ language 'plpgsql'
|
||||||
`);
|
|
||||||
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)
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to initialize VIP tables:', error);
|
console.error('❌ Failed to initialize VIP tables:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -589,13 +589,11 @@ class EnhancedDataService {
|
|||||||
// Default settings structure
|
// Default settings structure
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
aviationStackKey: process.env.AVIATIONSTACK_API_KEY || '',
|
aviationStackKey: '',
|
||||||
googleMapsKey: '',
|
googleMapsKey: '',
|
||||||
twilioKey: '',
|
twilioKey: '',
|
||||||
auth0Domain: process.env.AUTH0_DOMAIN || '',
|
googleClientId: '',
|
||||||
auth0ClientId: process.env.AUTH0_CLIENT_ID || '',
|
googleClientSecret: ''
|
||||||
auth0ClientSecret: process.env.AUTH0_CLIENT_SECRET || '',
|
|
||||||
auth0Audience: process.env.AUTH0_AUDIENCE || ''
|
|
||||||
},
|
},
|
||||||
systemSettings: {
|
systemSettings: {
|
||||||
defaultPickupLocation: '',
|
defaultPickupLocation: '',
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class FlightService {
|
|||||||
console.log('Note: Free tier returns recent flights only, not future scheduled flights');
|
console.log('Note: Free tier returns recent flights only, not future scheduled flights');
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const data: any = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
console.log('AviationStack response status:', response.status);
|
console.log('AviationStack response status:', response.status);
|
||||||
|
|
||||||
@@ -128,12 +128,12 @@ class FlightService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for API errors in response
|
// Check for API errors in response
|
||||||
if (data?.error) {
|
if (data.error) {
|
||||||
console.error('AviationStack API error:', data.error);
|
console.error('AviationStack API error:', data.error);
|
||||||
return null;
|
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!
|
// This is a valid flight number that exists!
|
||||||
console.log(`✅ Valid flight number: ${formattedFlightNumber} exists in the system`);
|
console.log(`✅ Valid flight number: ${formattedFlightNumber} exists in the system`);
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
target: development
|
target: development
|
||||||
|
environment:
|
||||||
|
VITE_API_URL: http://localhost:3000/api
|
||||||
ports:
|
ports:
|
||||||
- 5173:5173
|
- 5173:5173
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -6,55 +6,53 @@ services:
|
|||||||
image: postgres:15
|
image: postgres:15
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: vip_coordinator
|
POSTGRES_DB: vip_coordinator
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme}
|
||||||
volumes:
|
volumes:
|
||||||
- postgres-data:/var/lib/postgresql/data
|
- postgres-data:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7
|
image: redis:7
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
target: production
|
target: production
|
||||||
environment:
|
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
|
REDIS_URL: redis://redis:6379
|
||||||
NODE_ENV: production
|
JWT_SECRET: ${JWT_SECRET:-your-super-secure-jwt-secret-key-change-in-production-12345}
|
||||||
FRONTEND_URL: ${FRONTEND_URL}
|
SESSION_SECRET: ${SESSION_SECRET:-your-super-secure-session-secret-change-in-production-67890}
|
||||||
AUTH0_DOMAIN: ${AUTH0_DOMAIN}
|
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-308004695553-6k34bbq22frc4e76kejnkgq8mncepbbg.apps.googleusercontent.com}
|
||||||
AUTH0_CLIENT_ID: ${AUTH0_CLIENT_ID}
|
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-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}
|
||||||
AUTH0_CLIENT_SECRET: ${AUTH0_CLIENT_SECRET}
|
GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-https://api.bsa.madeamess.online/auth/google/callback}
|
||||||
AUTH0_AUDIENCE: ${AUTH0_AUDIENCE}
|
FRONTEND_URL: ${FRONTEND_URL:-https://bsa.madeamess.online}
|
||||||
INITIAL_ADMIN_EMAILS: ${INITIAL_ADMIN_EMAILS:-}
|
AVIATIONSTACK_API_KEY: ${AVIATIONSTACK_API_KEY:-your-aviationstack-api-key}
|
||||||
AVIATIONSTACK_API_KEY: ${AVIATIONSTACK_API_KEY:-}
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123}
|
||||||
DATABASE_SSL: ${DATABASE_SSL:-false}
|
PORT: ${PORT:-3000}
|
||||||
PGSSLMODE: disable
|
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
target: serve
|
target: production
|
||||||
args:
|
environment:
|
||||||
VITE_AUTH0_DOMAIN: ${AUTH0_DOMAIN}
|
VITE_API_URL: ${VITE_API_URL:-https://api.bsa.madeamess.online/api}
|
||||||
VITE_AUTH0_CLIENT_ID: ${AUTH0_CLIENT_ID}
|
|
||||||
VITE_AUTH0_AUDIENCE: ${AUTH0_AUDIENCE}
|
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 5173:5173
|
||||||
- 443:443
|
|
||||||
volumes:
|
|
||||||
- /opt/vip-coordinator/certs:/etc/nginx/certs:ro
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Multi-stage build for development and production
|
# Multi-stage build for development and production
|
||||||
FROM node:18-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -15,20 +15,7 @@ CMD ["npm", "run", "dev"]
|
|||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
ARG VITE_AUTH0_DOMAIN
|
RUN npm install
|
||||||
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
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
EXPOSE 5173
|
||||||
RUN npm prune --omit=dev
|
CMD ["npm", "run", "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;"]
|
|
||||||
|
|||||||
@@ -1,64 +1,51 @@
|
|||||||
worker_processes auto;
|
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
sendfile on;
|
sendfile on;
|
||||||
tcp_nopush on;
|
keepalive_timeout 65;
|
||||||
tcp_nodelay on;
|
|
||||||
keepalive_timeout 65;
|
# Gzip compression
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss image/svg+xml;
|
gzip_vary on;
|
||||||
gzip_min_length 256;
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||||
location /auth/callback {
|
|
||||||
try_files $uri /index.html;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -3,34 +3,38 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.0.0",
|
||||||
|
"npm": ">=10.0.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "node ./node_modules/vite/bin/vite.js",
|
||||||
"build": "vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth0/auth0-react": "^2.8.0",
|
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-leaflet": "^4.2.1",
|
"react-leaflet": "^4.2.1",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0"
|
||||||
"tailwindcss": "^3.4.14",
|
|
||||||
"vite": "^4.5.14"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/leaflet": "^1.9.4",
|
"@types/leaflet": "^1.9.4",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^8.15.0",
|
||||||
"@vitejs/plugin-react": "^4.0.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.3",
|
"eslint-plugin-react-refresh": "^0.4.14",
|
||||||
"postcss": "^8.4.47",
|
"@tailwindcss/postcss": "^4.1.8",
|
||||||
"typescript": "^5.0.2"
|
"postcss": "^8.5.4",
|
||||||
|
"tailwindcss": "^4.1.8",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"vite": "^5.4.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
frontend/postcss.config.mjs
Normal file
6
frontend/postcss.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,105 +1,117 @@
|
|||||||
/* Modern App-specific styles using Tailwind utilities */
|
/* Modern App-specific styles using Tailwind utilities */
|
||||||
|
|
||||||
/* Enhanced button styles */
|
/* Enhanced button styles */
|
||||||
.btn-modern {
|
@layer components {
|
||||||
@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-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-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-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-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;
|
.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 badges */
|
||||||
.status-badge {
|
@layer components {
|
||||||
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold;
|
.status-badge {
|
||||||
}
|
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
.status-scheduled {
|
.status-scheduled {
|
||||||
@apply bg-blue-100 text-blue-800 border border-blue-200;
|
@apply bg-blue-100 text-blue-800 border border-blue-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-in-progress {
|
.status-in-progress {
|
||||||
@apply bg-amber-100 text-amber-800 border border-amber-200;
|
@apply bg-amber-100 text-amber-800 border border-amber-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-completed {
|
.status-completed {
|
||||||
@apply bg-green-100 text-green-800 border border-green-200;
|
@apply bg-green-100 text-green-800 border border-green-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-cancelled {
|
.status-cancelled {
|
||||||
@apply bg-red-100 text-red-800 border border-red-200;
|
@apply bg-red-100 text-red-800 border border-red-200;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card enhancements */
|
/* Card enhancements */
|
||||||
.card-modern {
|
@layer components {
|
||||||
@apply bg-white rounded-2xl shadow-lg border border-slate-200/60 overflow-hidden backdrop-blur-sm;
|
.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-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;
|
.card-content {
|
||||||
|
@apply p-6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading states */
|
/* Loading states */
|
||||||
.loading-spinner {
|
@layer components {
|
||||||
@apply animate-spin rounded-full border-4 border-blue-600 border-t-transparent;
|
.loading-spinner {
|
||||||
}
|
@apply animate-spin rounded-full border-4 border-blue-600 border-t-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-text {
|
.loading-text {
|
||||||
@apply text-slate-600 animate-pulse;
|
@apply text-slate-600 animate-pulse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton {
|
.skeleton {
|
||||||
@apply animate-pulse bg-slate-200 rounded;
|
@apply animate-pulse bg-slate-200 rounded;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form enhancements */
|
/* Form enhancements */
|
||||||
.form-modern {
|
@layer components {
|
||||||
@apply space-y-6;
|
.form-modern {
|
||||||
}
|
@apply space-y-6;
|
||||||
|
}
|
||||||
.form-group-modern {
|
|
||||||
@apply space-y-2;
|
.form-group-modern {
|
||||||
}
|
@apply space-y-2;
|
||||||
|
}
|
||||||
.form-label-modern {
|
|
||||||
@apply block text-sm font-semibold text-slate-700;
|
.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-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;
|
.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 */
|
/* Animation utilities */
|
||||||
.animate-fade-in {
|
@layer utilities {
|
||||||
animation: fadeIn 0.5s ease-in-out;
|
.animate-fade-in {
|
||||||
}
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
}
|
||||||
.animate-slide-up {
|
|
||||||
animation: slideUp 0.3s ease-out;
|
.animate-slide-up {
|
||||||
}
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
.animate-scale-in {
|
|
||||||
animation: scaleIn 0.2s ease-out;
|
.animate-scale-in {
|
||||||
|
animation: scaleIn 0.2s ease-out;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
@@ -149,23 +161,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Glass morphism effect */
|
/* Glass morphism effect */
|
||||||
.glass {
|
@layer utilities {
|
||||||
@apply bg-white/80 backdrop-blur-lg border border-white/20;
|
.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;
|
.glass-dark {
|
||||||
|
@apply bg-slate-900/80 backdrop-blur-lg border border-slate-700/20;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover effects */
|
/* Hover effects */
|
||||||
.hover-lift {
|
@layer utilities {
|
||||||
@apply transition-transform duration-200 hover:-translate-y-1;
|
.hover-lift {
|
||||||
}
|
@apply transition-transform duration-200 hover:-translate-y-1;
|
||||||
|
}
|
||||||
.hover-glow {
|
|
||||||
@apply transition-shadow duration-200 hover:shadow-2xl;
|
.hover-glow {
|
||||||
}
|
@apply transition-shadow duration-200 hover:shadow-2xl;
|
||||||
|
}
|
||||||
.hover-scale {
|
|
||||||
@apply transition-transform duration-200 hover:scale-105;
|
.hover-scale {
|
||||||
|
@apply transition-transform duration-200 hover:scale-105;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
||||||
import { useAuth0 } from '@auth0/auth0-react';
|
|
||||||
import { apiCall } from './config/api';
|
import { apiCall } from './config/api';
|
||||||
import VipList from './pages/VipList';
|
import VipList from './pages/VipList';
|
||||||
import VipDetails from './pages/VipDetails';
|
import VipDetails from './pages/VipDetails';
|
||||||
@@ -12,102 +11,54 @@ import UserManagement from './components/UserManagement';
|
|||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
const AUTH0_AUDIENCE = import.meta.env.VITE_AUTH0_AUDIENCE;
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const {
|
|
||||||
isLoading: authLoading,
|
|
||||||
isAuthenticated,
|
|
||||||
loginWithRedirect,
|
|
||||||
logout,
|
|
||||||
getAccessTokenSilently,
|
|
||||||
user: auth0User,
|
|
||||||
error: authError
|
|
||||||
} = useAuth0();
|
|
||||||
|
|
||||||
const [user, setUser] = useState<any>(null);
|
const [user, setUser] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
|
||||||
const [pendingApproval, setPendingApproval] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const bootstrap = async () => {
|
// Check if user is already authenticated
|
||||||
if (!isAuthenticated) {
|
const token = localStorage.getItem('authToken');
|
||||||
setUser(null);
|
if (token) {
|
||||||
setStatusMessage(null);
|
apiCall('/auth/me', {
|
||||||
setPendingApproval(false);
|
headers: {
|
||||||
setLoading(false);
|
'Authorization': `Bearer ${token}`
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if (!response.ok) {
|
.then(res => {
|
||||||
throw new Error(`Failed to load profile (${response.status})`);
|
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();
|
.then(userData => {
|
||||||
const userRecord = data.user || data;
|
setUser(userData);
|
||||||
|
|
||||||
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 {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
})
|
||||||
};
|
.catch(error => {
|
||||||
|
console.error('Auth check failed:', error);
|
||||||
if (!authLoading) {
|
setLoading(false);
|
||||||
bootstrap();
|
});
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, authLoading, getAccessTokenSilently, auth0User]);
|
}, []);
|
||||||
|
|
||||||
|
const handleLogin = (userData: any) => {
|
||||||
|
setUser(userData);
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('authToken');
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
|
||||||
<div className="bg-white rounded-2xl shadow-xl p-8 flex items-center space-x-4">
|
<div className="bg-white rounded-2xl shadow-xl p-8 flex items-center space-x-4">
|
||||||
@@ -118,68 +69,23 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingApproval) {
|
// Handle OAuth callback route even when not logged in
|
||||||
return (
|
if (window.location.pathname === '/auth/callback' || window.location.pathname === '/auth/google/callback') {
|
||||||
<div className="min-h-screen bg-gradient-to-br from-amber-50 to-rose-50 flex justify-center items-center px-4">
|
return <Login onLogin={handleLogin} />;
|
||||||
<div className="bg-white border border-amber-200/60 rounded-2xl shadow-xl max-w-xl w-full p-8 space-y-4 text-center">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="w-16 h-16 rounded-full bg-amber-100 text-amber-600 flex items-center justify-center text-3xl">
|
|
||||||
⏳
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold text-slate-800">Awaiting Administrator Approval</h1>
|
|
||||||
<p className="text-slate-600">
|
|
||||||
{statusMessage ||
|
|
||||||
'Thanks for signing in. An administrator needs to approve your account before you can access the dashboard.'}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="btn btn-secondary mt-4"
|
|
||||||
>
|
|
||||||
Sign out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const beginLogin = async () => {
|
if (!user) {
|
||||||
try {
|
return <Login onLogin={handleLogin} />;
|
||||||
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 (
|
|
||||||
<Login
|
|
||||||
onLogin={beginLogin}
|
|
||||||
errorMessage={statusMessage || authError?.message}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50">
|
||||||
|
{/* Modern Navigation */}
|
||||||
<nav className="bg-white/80 backdrop-blur-lg border-b border-slate-200/60 sticky top-0 z-50">
|
<nav className="bg-white/80 backdrop-blur-lg border-b border-slate-200/60 sticky top-0 z-50">
|
||||||
<div className="max-w-7xl mx-auto px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-6 lg:px-8">
|
||||||
<div className="flex justify-between items-center h-16">
|
<div className="flex justify-between items-center h-16">
|
||||||
|
{/* Logo/Brand */}
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||||
<span className="text-white font-bold text-sm">VC</span>
|
<span className="text-white font-bold text-sm">VC</span>
|
||||||
@@ -189,36 +95,37 @@ function App() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Links */}
|
||||||
<div className="hidden md:flex items-center space-x-1">
|
<div className="hidden md:flex items-center space-x-1">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
|
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
|
||||||
>
|
>
|
||||||
Dashboard
|
Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/vips"
|
to="/vips"
|
||||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
|
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
|
||||||
>
|
>
|
||||||
VIPs
|
VIPs
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/drivers"
|
to="/drivers"
|
||||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
|
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-200"
|
||||||
>
|
>
|
||||||
Drivers
|
Drivers
|
||||||
</Link>
|
</Link>
|
||||||
{userRole === 'administrator' && (
|
{(user.role === 'administrator' || user.role === 'coordinator') && (
|
||||||
<Link
|
<Link
|
||||||
to="/admin"
|
to="/admin"
|
||||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-all duration-200"
|
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-amber-600 hover:bg-amber-50 rounded-lg transition-all duration-200"
|
||||||
>
|
>
|
||||||
Admin
|
Admin
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{userRole === 'administrator' && (
|
{user.role === 'administrator' && (
|
||||||
<Link
|
<Link
|
||||||
to="/users"
|
to="/users"
|
||||||
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-all duration-200"
|
className="px-4 py-2 text-sm font-medium text-slate-700 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-all duration-200"
|
||||||
>
|
>
|
||||||
Users
|
Users
|
||||||
@@ -226,23 +133,20 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* User Menu */}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="hidden sm:flex items-center space-x-3">
|
<div className="hidden sm:flex items-center space-x-3">
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-slate-400 to-slate-600 rounded-full flex items-center justify-center overflow-hidden">
|
<div className="w-8 h-8 bg-gradient-to-br from-slate-400 to-slate-600 rounded-full flex items-center justify-center">
|
||||||
{user.picture ? (
|
<span className="text-white text-xs font-medium">
|
||||||
<img src={user.picture} alt={displayName} className="w-8 h-8 object-cover" />
|
{user.name.charAt(0).toUpperCase()}
|
||||||
) : (
|
</span>
|
||||||
<span className="text-white text-xs font-medium">
|
|
||||||
{displayInitial}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<div className="font-medium text-slate-900">{displayName}</div>
|
<div className="font-medium text-slate-900">{user.name}</div>
|
||||||
<div className="text-slate-500 capitalize">{userRole}</div>
|
<div className="text-slate-500 capitalize">{user.role}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
|
className="bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||||
>
|
>
|
||||||
@@ -253,6 +157,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
<main className="max-w-7xl mx-auto px-6 lg:px-8 py-8">
|
<main className="max-w-7xl mx-auto px-6 lg:px-8 py-8">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
|
|||||||
@@ -94,101 +94,6 @@
|
|||||||
margin: 0;
|
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 {
|
.login-footer {
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import { apiCall } from '../config/api';
|
|||||||
import './Login.css';
|
import './Login.css';
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
onLogin: () => void;
|
onLogin: (user: any) => void;
|
||||||
errorMessage?: string | null | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Login: React.FC<LoginProps> = ({ onLogin, errorMessage }) => {
|
const Login: React.FC<LoginProps> = ({ onLogin }) => {
|
||||||
const [setupStatus, setSetupStatus] = useState<any>(null);
|
const [setupStatus, setSetupStatus] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Check system setup status
|
||||||
apiCall('/auth/setup')
|
apiCall('/auth/setup')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -22,7 +22,82 @@ const Login: React.FC<LoginProps> = ({ onLogin, errorMessage }) => {
|
|||||||
console.error('Error checking setup status:', error);
|
console.error('Error checking setup status:', error);
|
||||||
setLoading(false);
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -45,38 +120,55 @@ const Login: React.FC<LoginProps> = ({ onLogin, errorMessage }) => {
|
|||||||
{!setupStatus?.firstAdminCreated && (
|
{!setupStatus?.firstAdminCreated && (
|
||||||
<div className="setup-notice">
|
<div className="setup-notice">
|
||||||
<h3>🚀 First Time Setup</h3>
|
<h3>🚀 First Time Setup</h3>
|
||||||
<p>The first person to sign in will be promoted to administrator automatically.</p>
|
<p>The first person to log in will become the system administrator.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="login-content">
|
<div className="login-content">
|
||||||
<button
|
<button
|
||||||
className="google-login-btn"
|
className="google-login-btn"
|
||||||
onClick={onLogin}
|
onClick={handleGoogleLogin}
|
||||||
|
disabled={false}
|
||||||
>
|
>
|
||||||
<svg className="google-icon" viewBox="0 0 24 24">
|
<svg className="google-icon" viewBox="0 0 24 24">
|
||||||
<path fill="#635dff" d="M22 12.07c0-5.52-4.48-10-10-10s-10 4.48-10 10a9.97 9.97 0 006.85 9.48.73.73 0 00.95-.7v-3.05c-2.79.61-3.38-1.19-3.38-1.19-.46-1.17-1.12-1.49-1.12-1.49-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.37 1.09 2.96.83.09-.65.35-1.09.63-1.34-2.23-.25-4.57-1.12-4.57-4.96 0-1.1.39-2 1.03-2.7-.1-.25-.45-1.25.1-2.6 0 0 .84-.27 2.75 1.02a9.53 9.53 0 015 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.35.1 2.6.64.7 1.03 1.6 1.03 2.7 0 3.85-2.34 4.71-4.58 4.95.36.31.69.92.69 1.86v2.75c0 .39.27.71.66.79a10 10 0 007.61-9.71z"/>
|
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||||
|
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||||
|
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||||
|
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||||
</svg>
|
</svg>
|
||||||
Continue with Auth0
|
Continue with Google
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="login-info">
|
<div className="login-info">
|
||||||
<p>
|
<p>
|
||||||
{setupStatus?.authProvider === 'auth0'
|
{setupStatus?.firstAdminCreated
|
||||||
? 'Sign in with your organisation account. We use Auth0 for secure authentication.'
|
? "Sign in with your Google account to access the VIP Coordinator."
|
||||||
: 'Authentication service is being configured. Please try again later.'}
|
: "Sign in with Google to set up your administrator account."
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errorMessage && (
|
{setupStatus && !setupStatus.setupCompleted && (
|
||||||
<div className="dev-login-error" style={{ marginTop: '1rem' }}>
|
<div style={{
|
||||||
{errorMessage}
|
marginTop: '1rem',
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: '#fff3cd',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid #ffeaa7',
|
||||||
|
fontSize: '0.9rem'
|
||||||
|
}}>
|
||||||
|
<strong>⚠️ Setup Required:</strong>
|
||||||
|
<p style={{ margin: '0.5rem 0 0 0' }}>
|
||||||
|
Google OAuth credentials need to be configured. If the login doesn't work,
|
||||||
|
please follow the setup guide in <code>GOOGLE_OAUTH_SETUP.md</code> to configure
|
||||||
|
your Google Cloud Console credentials in the admin dashboard.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="login-footer">
|
<div className="login-footer">
|
||||||
<p>Secure authentication powered by Auth0</p>
|
<p>Secure authentication powered by Google OAuth</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
const DEFAULT_API_BASE =
|
// API Configuration
|
||||||
typeof window !== 'undefined'
|
// Use environment variable with fallback to production URL
|
||||||
? window.location.origin
|
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://api.bsa.madeamess.online';
|
||||||
: 'http://localhost:3000';
|
|
||||||
|
|
||||||
export const API_BASE_URL =
|
|
||||||
import.meta.env.VITE_API_URL?.replace(/\/$/, '') || DEFAULT_API_BASE;
|
|
||||||
|
|
||||||
|
// Helper function for API calls
|
||||||
export const apiCall = (endpoint: string, options?: RequestInit) => {
|
export const apiCall = (endpoint: string, options?: RequestInit) => {
|
||||||
const url = /^https?:\/\//.test(endpoint)
|
const url = endpoint.startsWith('/') ? `${API_BASE_URL}${endpoint}` : endpoint;
|
||||||
? endpoint
|
|
||||||
: `${API_BASE_URL}${endpoint}`;
|
|
||||||
return fetch(url, options);
|
return fetch(url, options);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,49 +1,4 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom base styles */
|
/* Custom base styles */
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -92,117 +47,304 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
/* Modern Button Styles */
|
/* Modern Button Styles */
|
||||||
.btn {
|
.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 {
|
.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 {
|
.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 {
|
.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 {
|
.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 */
|
/* Modern Card Styles */
|
||||||
.card {
|
.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 */
|
/* Modern Form Styles */
|
||||||
.form-group {
|
.form-group {
|
||||||
@apply mb-6;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.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 {
|
.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 {
|
.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 {
|
.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 {
|
.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 {
|
.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 Styles */
|
||||||
.modal-overlay {
|
.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 {
|
.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 {
|
.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 {
|
.modal-body {
|
||||||
@apply p-8;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.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 */
|
||||||
.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 Sections */
|
||||||
.form-section {
|
.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 {
|
.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 {
|
.form-section-title {
|
||||||
@apply text-lg font-bold text-slate-800;
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Radio Group */
|
/* Radio Group */
|
||||||
.radio-group {
|
.radio-group {
|
||||||
@apply flex gap-6 mt-3;
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-option {
|
.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 {
|
.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 Group */
|
||||||
.checkbox-option {
|
.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 {
|
.checkbox-option.checked {
|
||||||
@apply border-blue-500 bg-blue-50;
|
border-color: #3b82f6;
|
||||||
|
background-color: #eff6ff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client'
|
||||||
import { Auth0Provider } from '@auth0/auth0-react';
|
import App from './App.tsx'
|
||||||
import App from './App.tsx';
|
import './index.css'
|
||||||
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<string, string> = {
|
|
||||||
redirect_uri: `${window.location.origin}/auth/callback`,
|
|
||||||
scope: 'openid profile email'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (audience) {
|
|
||||||
authorizationParams.audience = audience;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Auth0Provider
|
<App />
|
||||||
domain={domain}
|
</React.StrictMode>,
|
||||||
clientId={clientId}
|
)
|
||||||
authorizationParams={authorizationParams}
|
|
||||||
cacheLocation="localstorage"
|
|
||||||
useRefreshTokens={true}
|
|
||||||
>
|
|
||||||
<App />
|
|
||||||
</Auth0Provider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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';
|
import { generateTestVips, getTestOrganizations, generateVipSchedule } from '../utils/testVipData';
|
||||||
|
|
||||||
interface ApiKeys {
|
interface ApiKeys {
|
||||||
aviationStackKey?: string;
|
aviationStackKey?: string;
|
||||||
googleMapsKey?: string;
|
googleMapsKey?: string;
|
||||||
twilioKey?: string;
|
twilioKey?: string;
|
||||||
auth0Domain?: string;
|
googleClientId?: string;
|
||||||
auth0ClientId?: string;
|
googleClientSecret?: string;
|
||||||
auth0ClientSecret?: string;
|
|
||||||
auth0Audience?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SystemSettings {
|
interface SystemSettings {
|
||||||
@@ -22,89 +20,92 @@ interface SystemSettings {
|
|||||||
|
|
||||||
const AdminDashboard: React.FC = () => {
|
const AdminDashboard: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [adminPassword, setAdminPassword] = useState('');
|
||||||
const [apiKeys, setApiKeys] = useState<ApiKeys>({});
|
const [apiKeys, setApiKeys] = useState<ApiKeys>({});
|
||||||
const [systemSettings, setSystemSettings] = useState<SystemSettings>({});
|
const [systemSettings, setSystemSettings] = useState<SystemSettings>({});
|
||||||
const [testResults, setTestResults] = useState<{ [key: string]: string }>({});
|
const [testResults, setTestResults] = useState<{ [key: string]: string }>({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [saveStatus, setSaveStatus] = useState<string | null>(null);
|
const [saveStatus, setSaveStatus] = useState<string | null>(null);
|
||||||
|
const [showKeys, setShowKeys] = useState<{ [key: string]: boolean }>({});
|
||||||
const [savedKeys, setSavedKeys] = useState<{ [key: string]: boolean }>({});
|
const [savedKeys, setSavedKeys] = useState<{ [key: string]: boolean }>({});
|
||||||
const [maskedKeyHints, setMaskedKeyHints] = useState<{ [key: string]: string }>({});
|
|
||||||
const [testDataLoading, setTestDataLoading] = useState(false);
|
const [testDataLoading, setTestDataLoading] = useState(false);
|
||||||
const [testDataStatus, setTestDataStatus] = useState<string | null>(null);
|
const [testDataStatus, setTestDataStatus] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const buildAuthHeaders = (includeJson = false) => {
|
useEffect(() => {
|
||||||
const headers: Record<string, string> = {};
|
// Check if already authenticated
|
||||||
const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
|
const authStatus = sessionStorage.getItem('adminAuthenticated');
|
||||||
if (token) {
|
if (authStatus === 'true') {
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
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 () => {
|
const loadSettings = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
const response = await fetch('/api/admin/settings', {
|
||||||
setError(null);
|
headers: {
|
||||||
const response = await apiCall('/api/admin/settings', {
|
'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || ''
|
||||||
headers: buildAuthHeaders()
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
// Track which keys are already saved (masked keys start with ***)
|
||||||
const saved: { [key: string]: boolean } = {};
|
const saved: { [key: string]: boolean } = {};
|
||||||
const maskedHints: { [key: string]: string } = {};
|
|
||||||
const cleanedApiKeys: ApiKeys = {};
|
|
||||||
|
|
||||||
if (data.apiKeys) {
|
if (data.apiKeys) {
|
||||||
Object.entries(data.apiKeys).forEach(([key, value]) => {
|
Object.entries(data.apiKeys).forEach(([key, value]) => {
|
||||||
if (value && (value as string).startsWith('***')) {
|
if (value && (value as string).startsWith('***')) {
|
||||||
saved[key] = true;
|
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;
|
cleanedApiKeys[key as keyof ApiKeys] = value as string;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setSavedKeys(saved);
|
|
||||||
setMaskedKeyHints(maskedHints);
|
|
||||||
setApiKeys(cleanedApiKeys);
|
setApiKeys(cleanedApiKeys);
|
||||||
setSystemSettings(data.systemSettings || {});
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to load settings:', err);
|
console.error('Failed to load settings:', error);
|
||||||
setError('Failed to load admin settings.');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadSettings();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleApiKeyChange = (key: keyof ApiKeys, value: string) => {
|
const handleApiKeyChange = (key: keyof ApiKeys, value: string) => {
|
||||||
setApiKeys(prev => ({ ...prev, [key]: value }));
|
setApiKeys(prev => ({ ...prev, [key]: value }));
|
||||||
// If user is typing a new key, mark it as not saved anymore
|
// If user is typing a new key, mark it as not saved anymore
|
||||||
if (value && !value.startsWith('***')) {
|
if (value && !value.startsWith('***')) {
|
||||||
setSavedKeys(prev => ({ ...prev, [key]: false }));
|
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...' }));
|
setTestResults(prev => ({ ...prev, [apiType]: 'Testing...' }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiCall(`/api/admin/test-api/${apiType}`, {
|
const response = await fetch(`/api/admin/test-api/${apiType}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: buildAuthHeaders(true),
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || ''
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
apiKey: apiKeys[apiType as keyof ApiKeys]
|
apiKey: apiKeys[apiType as keyof ApiKeys]
|
||||||
})
|
})
|
||||||
@@ -146,13 +150,16 @@ const AdminDashboard: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveSettings = async () => {
|
const saveSettings = async () => {
|
||||||
setSaving(true);
|
setLoading(true);
|
||||||
setSaveStatus(null);
|
setSaveStatus(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiCall('/api/admin/settings', {
|
const response = await fetch('/api/admin/settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: buildAuthHeaders(true),
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Admin-Auth': sessionStorage.getItem('adminAuthenticated') || ''
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
apiKeys,
|
apiKeys,
|
||||||
systemSettings
|
systemSettings
|
||||||
@@ -161,8 +168,16 @@ const AdminDashboard: React.FC = () => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setSaveStatus('Settings saved successfully!');
|
setSaveStatus('Settings saved successfully!');
|
||||||
// Refresh the latest settings so saved states/labels stay accurate
|
// Mark keys as saved if they have values
|
||||||
await loadSettings();
|
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);
|
setTimeout(() => setSaveStatus(null), 3000);
|
||||||
} else {
|
} else {
|
||||||
setSaveStatus('Failed to save settings');
|
setSaveStatus('Failed to save settings');
|
||||||
@@ -170,14 +185,14 @@ const AdminDashboard: React.FC = () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSaveStatus('Error saving settings');
|
setSaveStatus('Error saving settings');
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('authToken');
|
sessionStorage.removeItem('adminAuthenticated');
|
||||||
|
setIsAuthenticated(false);
|
||||||
navigate('/');
|
navigate('/');
|
||||||
window.location.reload();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test VIP functions
|
// Test VIP functions
|
||||||
@@ -337,29 +352,37 @@ const AdminDashboard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
|
||||||
<div className="bg-white rounded-2xl shadow-xl p-8 flex items-center space-x-4 border border-slate-200/60">
|
<div className="bg-white rounded-2xl shadow-xl p-8 w-full max-w-md border border-slate-200/60">
|
||||||
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin"></div>
|
<div className="text-center mb-8">
|
||||||
<span className="text-lg font-medium text-slate-700">Loading admin settings...</span>
|
<div className="w-16 h-16 bg-gradient-to-br from-amber-500 to-orange-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
</div>
|
<div className="w-8 h-8 bg-white rounded-full flex items-center justify-center">
|
||||||
</div>
|
<div className="w-4 h-4 bg-amber-500 rounded-full"></div>
|
||||||
);
|
</div>
|
||||||
}
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-slate-800">Admin Login</h2>
|
||||||
if (error) {
|
<p className="text-slate-600 mt-2">Enter your admin password to continue</p>
|
||||||
return (
|
</div>
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex justify-center items-center">
|
|
||||||
<div className="bg-white rounded-2xl shadow-xl p-8 w-full max-w-xl border border-rose-200/70">
|
<form onSubmit={handleLogin} className="space-y-6">
|
||||||
<h2 className="text-2xl font-bold text-rose-700 mb-4">Admin access required</h2>
|
<div className="form-group">
|
||||||
<p className="text-slate-600 mb-6">{error}</p>
|
<label htmlFor="password" className="form-label">Admin Password</label>
|
||||||
<button
|
<input
|
||||||
className="btn btn-primary"
|
type="password"
|
||||||
onClick={() => navigate('/')}
|
id="password"
|
||||||
>
|
value={adminPassword}
|
||||||
Return to dashboard
|
onChange={(e) => setAdminPassword(e.target.value)}
|
||||||
</button>
|
className="form-input"
|
||||||
|
placeholder="Enter admin password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="btn btn-primary w-full">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -415,20 +438,24 @@ const AdminDashboard: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 items-end">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 items-end">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<label className="form-label">API Key</label>
|
<label className="form-label">API Key</label>
|
||||||
<input
|
<div className="relative">
|
||||||
type="password"
|
<input
|
||||||
placeholder={savedKeys.aviationStackKey && maskedKeyHints.aviationStackKey
|
type={showKeys.aviationStackKey ? 'text' : 'password'}
|
||||||
? `Saved (${maskedKeyHints.aviationStackKey.slice(-4)})`
|
placeholder={savedKeys.aviationStackKey ? 'Key saved (enter new key to update)' : 'Enter AviationStack API key'}
|
||||||
: 'Enter AviationStack API key'}
|
value={apiKeys.aviationStackKey || ''}
|
||||||
value={apiKeys.aviationStackKey || ''}
|
onChange={(e) => handleApiKeyChange('aviationStackKey', e.target.value)}
|
||||||
onChange={(e) => handleApiKeyChange('aviationStackKey', e.target.value)}
|
className="form-input pr-12"
|
||||||
className="form-input"
|
/>
|
||||||
/>
|
{savedKeys.aviationStackKey && (
|
||||||
{savedKeys.aviationStackKey && maskedKeyHints.aviationStackKey && !apiKeys.aviationStackKey && (
|
<button
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
type="button"
|
||||||
Currently saved key ends with {maskedKeyHints.aviationStackKey.slice(-4)}. Enter a new value to replace it.
|
onClick={() => setShowKeys(prev => ({ ...prev, aviationStackKey: !prev.aviationStackKey }))}
|
||||||
</p>
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||||
)}
|
>
|
||||||
|
{showKeys.aviationStackKey ? 'Hide' : 'Show'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
Get your key from: https://aviationstack.com/dashboard
|
Get your key from: https://aviationstack.com/dashboard
|
||||||
</p>
|
</p>
|
||||||
@@ -457,91 +484,72 @@ const AdminDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Auth0 Credentials */}
|
{/* Google OAuth Credentials */}
|
||||||
<div className="form-section">
|
<div className="form-section">
|
||||||
<div className="form-section-header">
|
<div className="form-section-header">
|
||||||
<h3 className="form-section-title">Auth0 Configuration</h3>
|
<h3 className="form-section-title">Google OAuth Credentials</h3>
|
||||||
{(savedKeys.auth0Domain || savedKeys.auth0ClientId || savedKeys.auth0ClientSecret) && (
|
{(savedKeys.googleClientId && savedKeys.googleClientSecret) && (
|
||||||
<span className="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
<span className="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||||
Configured
|
Configured
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Auth0 Domain</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={savedKeys.auth0Domain && maskedKeyHints.auth0Domain
|
|
||||||
? `Saved (${maskedKeyHints.auth0Domain.slice(-4)})`
|
|
||||||
: 'e.g. dev-1234abcd.us.auth0.com'}
|
|
||||||
value={apiKeys.auth0Domain || ''}
|
|
||||||
onChange={(e) => handleApiKeyChange('auth0Domain', e.target.value)}
|
|
||||||
className="form-input"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Client ID</label>
|
<label className="form-label">Client ID</label>
|
||||||
<input
|
<div className="relative">
|
||||||
type="password"
|
<input
|
||||||
placeholder={savedKeys.auth0ClientId && maskedKeyHints.auth0ClientId
|
type={showKeys.googleClientId ? 'text' : 'password'}
|
||||||
? `Saved (${maskedKeyHints.auth0ClientId.slice(-4)})`
|
placeholder={savedKeys.googleClientId ? 'Client ID saved' : 'Enter Google OAuth Client ID'}
|
||||||
: 'Enter Auth0 application Client ID'}
|
value={apiKeys.googleClientId || ''}
|
||||||
value={apiKeys.auth0ClientId || ''}
|
onChange={(e) => handleApiKeyChange('googleClientId', e.target.value)}
|
||||||
onChange={(e) => handleApiKeyChange('auth0ClientId', e.target.value)}
|
className="form-input pr-12"
|
||||||
className="form-input"
|
/>
|
||||||
/>
|
{savedKeys.googleClientId && (
|
||||||
{savedKeys.auth0ClientId && maskedKeyHints.auth0ClientId && !apiKeys.auth0ClientId && (
|
<button
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
type="button"
|
||||||
Saved client ID ends with {maskedKeyHints.auth0ClientId.slice(-4)}. Provide a new ID to update it.
|
onClick={() => setShowKeys(prev => ({ ...prev, googleClientId: !prev.googleClientId }))}
|
||||||
</p>
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||||
)}
|
>
|
||||||
|
{showKeys.googleClientId ? 'Hide' : 'Show'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Client Secret</label>
|
<label className="form-label">Client Secret</label>
|
||||||
<input
|
<div className="relative">
|
||||||
type="password"
|
<input
|
||||||
placeholder={savedKeys.auth0ClientSecret && maskedKeyHints.auth0ClientSecret
|
type={showKeys.googleClientSecret ? 'text' : 'password'}
|
||||||
? `Saved (${maskedKeyHints.auth0ClientSecret.slice(-4)})`
|
placeholder={savedKeys.googleClientSecret ? 'Client Secret saved' : 'Enter Google OAuth Client Secret'}
|
||||||
: 'Enter Auth0 application Client Secret'}
|
value={apiKeys.googleClientSecret || ''}
|
||||||
value={apiKeys.auth0ClientSecret || ''}
|
onChange={(e) => handleApiKeyChange('googleClientSecret', e.target.value)}
|
||||||
onChange={(e) => handleApiKeyChange('auth0ClientSecret', e.target.value)}
|
className="form-input pr-12"
|
||||||
className="form-input"
|
/>
|
||||||
/>
|
{savedKeys.googleClientSecret && (
|
||||||
{savedKeys.auth0ClientSecret && maskedKeyHints.auth0ClientSecret && !apiKeys.auth0ClientSecret && (
|
<button
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
type="button"
|
||||||
Saved client secret ends with {maskedKeyHints.auth0ClientSecret.slice(-4)}. Provide a new secret to rotate it.
|
onClick={() => setShowKeys(prev => ({ ...prev, googleClientSecret: !prev.googleClientSecret }))}
|
||||||
</p>
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||||
)}
|
>
|
||||||
</div>
|
{showKeys.googleClientSecret ? 'Hide' : 'Show'}
|
||||||
|
</button>
|
||||||
<div className="form-group">
|
)}
|
||||||
<label className="form-label">API Audience (Identifier)</label>
|
</div>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={apiKeys.auth0Audience || 'https://your-api-identifier'}
|
|
||||||
value={apiKeys.auth0Audience || ''}
|
|
||||||
onChange={(e) => handleApiKeyChange('auth0Audience', e.target.value)}
|
|
||||||
className="form-input"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
|
||||||
Create an API in Auth0 and use its Identifier here (e.g. https://vip-coordinator-api).
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-4">
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-4">
|
||||||
<h4 className="font-semibold text-blue-900 mb-2">Setup Instructions</h4>
|
<h4 className="font-semibold text-blue-900 mb-2">Setup Instructions</h4>
|
||||||
<ol className="text-sm text-blue-800 space-y-1 list-decimal list-inside">
|
<ol className="text-sm text-blue-800 space-y-1 list-decimal list-inside">
|
||||||
<li>Sign in to the Auth0 Dashboard</li>
|
<li>Go to Google Cloud Console</li>
|
||||||
<li>Create a <strong>Single Page Application</strong> for the frontend</li>
|
<li>Create or select a project</li>
|
||||||
<li>Set Allowed Callback URL to <code>https://bsa.madeamess.online/auth/callback</code></li>
|
<li>Enable the Google+ API</li>
|
||||||
<li>Set Allowed Logout URL to <code>https://bsa.madeamess.online/</code></li>
|
<li>Go to "Credentials" → "Create Credentials" → "OAuth 2.0 Client IDs"</li>
|
||||||
<li>Set Allowed Web Origins to <code>https://bsa.madeamess.online</code></li>
|
<li>Set authorized redirect URI: http://bsa.madeamess.online:3000/auth/google/callback</li>
|
||||||
<li>Create an <strong>API</strong> in Auth0 for the backend and use its Identifier as the audience</li>
|
<li>Set authorized JavaScript origins: http://bsa.madeamess.online:5173</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -751,7 +759,7 @@ const AdminDashboard: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary w-full mb-2"
|
className="btn btn-primary w-full mb-2"
|
||||||
onClick={() => window.open(`${API_BASE_URL}/api-docs.html`, '_blank')}
|
onClick={() => window.open('http://localhost:3000/api-docs.html', '_blank')}
|
||||||
>
|
>
|
||||||
Open API Documentation
|
Open API Documentation
|
||||||
</button>
|
</button>
|
||||||
@@ -803,9 +811,9 @@ const AdminDashboard: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
className="btn btn-success text-lg px-8 py-4"
|
className="btn btn-success text-lg px-8 py-4"
|
||||||
onClick={saveSettings}
|
onClick={saveSettings}
|
||||||
disabled={saving}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{saving ? 'Saving...' : 'Save All Settings'}
|
{loading ? 'Saving...' : 'Save All Settings'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{saveStatus && (
|
{saveStatus && (
|
||||||
|
|||||||
@@ -8,5 +8,4 @@ export default {
|
|||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
}
|
||||||
|
|
||||||
@@ -3,9 +3,11 @@ import react from '@vitejs/plugin-react'
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
],
|
||||||
css: {
|
css: {
|
||||||
postcss: './postcss.config.js',
|
postcss: './postcss.config.mjs',
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
@@ -45,10 +47,6 @@ export default defineConfig({
|
|||||||
target: 'http://backend:3000',
|
target: 'http://backend:3000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/auth/dev-login': {
|
|
||||||
target: 'http://backend:3000',
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user