diff --git a/.env.example b/.env.example index 83dcc1f..de16f2e 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ DB_HOST=localhost DB_PORT=5432 DB_NAME=shattered_void_dev DB_USER=postgres -DB_PASSWORD=s5d7dfs5e2q23 +DB_PASSWORD=password # Redis Configuration REDIS_HOST=localhost diff --git a/STARTUP_GUIDE.md b/STARTUP_GUIDE.md deleted file mode 100644 index 51cfd8b..0000000 --- a/STARTUP_GUIDE.md +++ /dev/null @@ -1,568 +0,0 @@ -# Shattered Void MMO - Startup System Guide - -This guide covers the comprehensive startup system for the Shattered Void MMO, providing multiple ways to launch and manage the game services. - -## Quick Start - -The easiest way to start the game: - -```bash -# Simple startup with all default settings -./start.sh - -# Or using npm -npm run game -``` - -## Startup Options - -### Shell Script (Recommended) - -The `start.sh` script provides the most user-friendly interface: - -```bash -# Development mode (default) -./start.sh - -# Production mode -./start.sh --env production - -# Debug mode with verbose logging -./start.sh --debug --verbose - -# Backend only (no frontend) -./start.sh --no-frontend - -# Custom port -./start.sh --port 8080 - -# Skip database checks (useful for testing) -./start.sh --no-database --skip-preflight -``` - -### NPM Scripts - -```bash -# Comprehensive startup with full system validation -npm run start:game - -# Environment-specific startup -npm run start:dev # Development mode -npm run start:prod # Production mode -npm run start:staging # Staging mode - -# Quick startup (shell script) -npm run start:quick - -# Debug mode -npm run start:debug - -# Backend only -npm run start:backend-only -``` - -### Direct Node.js - -```bash -# Direct startup (bypasses some safety checks) -node start-game.js - -# With environment -NODE_ENV=production node start-game.js -``` - -## Configuration - -### Environment Variables - -The startup system respects these environment variables: - -```bash -# Core settings -NODE_ENV=development|production|staging|testing -PORT=3000 # Backend port -FRONTEND_PORT=5173 # Frontend port -HOST=0.0.0.0 # Host binding - -# Service toggles -ENABLE_FRONTEND=true|false # Enable/disable frontend -DISABLE_DATABASE=true|false # Skip database -DISABLE_REDIS=true|false # Skip Redis -ENABLE_HEALTH_MONITORING=true|false # Health checks - -# Startup behavior -SKIP_PREFLIGHT=true|false # Skip system checks -VERBOSE_STARTUP=true|false # Detailed logging -AUTO_MIGRATE=true|false # Auto-run migrations -AUTO_SEED=true|false # Auto-run seeds - -# Visual settings -DISABLE_BANNER=true|false # Hide startup banner -DISABLE_COLORS=true|false # Disable colored output -``` - -### Configuration File - -Advanced configuration is available in `config/startup.config.js`: - -```javascript -// Example: Custom timeout settings -const config = { - backend: { - startupTimeout: 30000, // 30 seconds - healthEndpoint: '/health' - }, - database: { - migrationTimeout: 60000, // 60 seconds - autoMigrate: true - }, - healthMonitoring: { - interval: 30000, // 30 seconds - alertThresholds: { - responseTime: 5000, // 5 seconds - memoryUsage: 80 // 80% - } - } -}; -``` - -## System Components - -### 1. Pre-flight Checks (`scripts/startup-checks.js`) - -Validates system requirements before startup: - -- ✅ Node.js version (18+) -- ✅ NPM availability -- ✅ Environment configuration -- ✅ Directory structure -- ✅ Package dependencies -- ✅ Port availability -- ✅ Database configuration -- ✅ Redis configuration (optional) -- ✅ Log directories -- ✅ Frontend dependencies (optional) -- ✅ System memory (1GB+ recommended) -- ✅ Disk space (<90% usage) -- ✅ File permissions - -Test individually: -```bash -npm run system:check -``` - -### 2. Database Validation (`scripts/database-validator.js`) - -Comprehensive database health checks: - -- 🔗 Connectivity testing -- 📦 Migration status and auto-execution -- 🏗️ Schema structure validation -- 🌱 Seed data verification -- 🔍 Data integrity checks -- 📊 Performance metrics - -Test individually: -```bash -npm run db:validate -``` - -### 3. Health Monitoring (`scripts/health-monitor.js`) - -Real-time service monitoring: - -- 🏥 Service health checks -- 📈 Performance metrics -- 🚨 Alert system -- 📊 Uptime tracking -- 💾 System resource monitoring - -Test individually: -```bash -npm run health:check -``` - -### 4. Main Orchestrator (`start-game.js`) - -Central startup coordination: - -- 🎭 Phase-based startup -- ⏱️ Timeout management -- 🔄 Retry logic -- 📝 Comprehensive logging -- 🛑 Graceful shutdown -- 📊 Performance metrics -- 🔧 Node.js version compatibility detection -- 📦 Automatic frontend fallback for older Node.js versions - -## Node.js Version Compatibility - -The system automatically detects Node.js version compatibility and handles Vite development server limitations: - -### Vite Development Server Requirements - -- **Node.js 20+**: Full Vite development server support -- **Node.js 18-19**: Automatic fallback to built frontend static server -- **Node.js <18**: Not supported - -### Automatic Fallback Behavior - -When Node.js version is incompatible with Vite 7.x (requires `crypto.hash()` from Node.js 20+): - -1. **Detection**: System detects Node.js version during startup -2. **Warning**: Clear warning about version compatibility -3. **Fallback**: Automatically serves built frontend from `/frontend/dist/` -4. **Status**: Frontend shows as "static" mode in startup summary - -```bash -# Example startup output with Node.js 18.x -Node.js version: v18.19.1 -Node.js v18.19.1 is not compatible with Vite 7.x (requires Node.js 20+) -crypto.hash() function is not available in this Node.js version -Attempting to serve built frontend as fallback... -Built frontend fallback started in 5ms - -║ ✅ Frontend:5173 (static) ║ -``` - -### Configuration Options - -Control fallback behavior with environment variables: - -```bash -# Disable frontend fallback (fail if Vite incompatible) -FRONTEND_FALLBACK=false ./start.sh - -# Force use built frontend even with compatible Node.js -# (automatically happens if Vite dev server fails for any reason) -``` - -### Building Frontend for Fallback - -Ensure built frontend is available: - -```bash -# Build frontend for production/fallback use -cd frontend -npm run build - -# Verify build exists -ls -la dist/ -``` - -## Startup Phases - -The startup system follows these phases: - -1. **🔍 Pre-flight Checks** - System validation -2. **🗄️ Database Validation** - DB connectivity and migrations -3. **🖥️ Backend Server Startup** - Express server launch -4. **🌐 Frontend Server Startup** - React dev server (if enabled) -5. **🏥 Health Monitoring** - Service monitoring activation - -Each phase includes: -- ⏱️ Timing metrics -- 🔄 Retry logic -- ❌ Error handling -- 📊 Progress reporting - -## Service Management - -### Starting Services - -```bash -# Full stack (backend + frontend + monitoring) -./start.sh - -# Backend only -./start.sh --no-frontend - -# Skip health monitoring -./start.sh --no-health - -# Database-free mode (for testing) -./start.sh --no-database -``` - -### Stopping Services - -```bash -# Graceful shutdown -Ctrl+C - -# Force stop (if needed) -pkill -f start-game.js -``` - -### Service Status - -The startup system provides real-time status: - -``` -╔═══════════════════════════════════════════════════════════════╗ -║ STARTUP SUMMARY ║ -╠═══════════════════════════════════════════════════════════════╣ -║ Total Duration: 2847ms ║ -║ ║ -║ Services Status: ║ -║ ✅ Preflight ║ -║ ✅ Database ║ -║ ✅ Backend:3000 ║ -║ ✅ Frontend:5173 ║ -║ ✅ HealthMonitor ║ -╚═══════════════════════════════════════════════════════════════╝ -``` - -## Troubleshooting - -### Common Issues - -1. **Port already in use** - ```bash - # Use different port - ./start.sh --port 8080 - - # Or kill existing process - lsof -ti:3000 | xargs kill - ``` - -2. **Database connection failed** - ```bash - # Check PostgreSQL status - sudo systemctl status postgresql - - # Start PostgreSQL - sudo systemctl start postgresql - - # Create database - createdb shattered_void_dev - ``` - -3. **Missing dependencies** - ```bash - # Install dependencies - npm install - - # Install frontend dependencies - cd frontend && npm install - ``` - -4. **Migration issues** - ```bash - # Reset database - npm run db:reset - - # Manual migration - npm run db:migrate - ``` - -5. **Vite development server fails (Node.js compatibility)** - ```bash - # Check Node.js version - node --version - - # If Node.js < 20, system will automatically fallback - # To upgrade Node.js: - # Using nvm: - nvm install 20 - nvm use 20 - - # Using package manager: - # Ubuntu/Debian: sudo apt update && sudo apt install nodejs - # MacOS: brew install node@20 - - # Verify fallback works by ensuring frontend is built: - cd frontend && npm run build - ``` - -6. **Frontend fallback not working** - ```bash - # Ensure frontend is built - cd frontend - npm install - npm run build - - # Verify build directory exists - ls -la dist/ - - # Check if Express is available (should be in package.json) - npm list express - ``` - -### Debug Mode - -Enable comprehensive debugging: - -```bash -# Maximum verbosity -./start.sh --debug --verbose - -# Or with environment variables -DEBUG=* VERBOSE_STARTUP=true ./start.sh -``` - -### Logs - -Access different log streams: - -```bash -# Combined logs -npm run logs - -# Error logs only -npm run logs:error - -# Startup logs -npm run logs:startup - -# Audit logs -npm run logs:audit -``` - -### Health Check Endpoints - -Once running, access health information: - -```bash -# Backend health -curl http://localhost:3000/health - -# Health monitoring data (if debug endpoints enabled) -curl http://localhost:3000/debug/health -``` - -## Production Deployment - -### Production Mode - -```bash -# Production startup -./start.sh --env production - -# Or with npm -npm run start:prod -``` - -Production mode changes: -- 🚫 Frontend disabled (serves pre-built assets) -- ⚡ Faster health check intervals -- 🔒 Enhanced security checks -- 📊 Performance monitoring enabled -- 🚨 Stricter error handling - -### Environment Variables for Production - -```bash -NODE_ENV=production -DISABLE_FRONTEND=true # Use nginx/CDN for frontend -ENABLE_HEALTH_MONITORING=true -LOG_LEVEL=warn -CRASH_REPORTING=true -PERFORMANCE_REPORTING=true -``` - -### Docker Integration - -The startup system works with Docker: - -```bash -# Build Docker image -npm run docker:build - -# Run with Docker Compose -npm run docker:run -``` - -## Development Tips - -### Quick Development Cycle - -```bash -# Fast startup without full checks -SKIP_PREFLIGHT=true ./start.sh --no-frontend - -# Backend only with auto-restart -npm run dev -``` - -### Testing the Startup System - -```bash -# Test all components -npm run system:check # Pre-flight checks -npm run db:validate # Database validation -npm run health:check # Health monitoring - -# Test specific scenarios -./start.sh --no-database --skip-preflight # Minimal startup -./start.sh --debug --log-file startup.log # Full logging -``` - -### Customizing the Startup - -Modify `config/startup.config.js` for custom behavior: - -```javascript -module.exports = { - backend: { - startupTimeout: 45000, // Longer timeout - port: 8080 // Different default port - }, - preflightChecks: { - enabled: false // Skip checks in development - } -}; -``` - -## API Reference - -### Startup Script Options - -| Option | Environment Variable | Description | -|--------|---------------------|-------------| -| `--env ENV` | `NODE_ENV` | Set environment mode | -| `--port PORT` | `PORT` | Backend server port | -| `--frontend-port PORT` | `FRONTEND_PORT` | Frontend server port | -| `--no-frontend` | `ENABLE_FRONTEND=false` | Disable frontend | -| `--no-health` | `ENABLE_HEALTH_MONITORING=false` | Disable health monitoring | -| `--no-database` | `DISABLE_DATABASE=true` | Skip database | -| `--no-redis` | `DISABLE_REDIS=true` | Skip Redis | -| `--skip-preflight` | `SKIP_PREFLIGHT=true` | Skip system checks | -| `--verbose` | `VERBOSE_STARTUP=true` | Enable verbose logging | -| `--debug` | `DEBUG=*` | Enable debug mode | -| `--no-colors` | `DISABLE_COLORS=true` | Disable colored output | - -### NPM Scripts Reference - -| Script | Description | -|--------|-------------| -| `npm run game` | Quick startup (shell script) | -| `npm run start:game` | Full startup with validation | -| `npm run start:dev` | Development mode | -| `npm run start:prod` | Production mode | -| `npm run start:debug` | Debug mode | -| `npm run start:backend-only` | Backend only | -| `npm run system:check` | Run system checks | -| `npm run health:check` | Test health monitoring | -| `npm run db:validate` | Validate database | - -## Contributing - -When modifying the startup system: - -1. **Test all scenarios** - Test with different combinations of flags -2. **Update documentation** - Keep this guide current -3. **Maintain backward compatibility** - Don't break existing workflows -4. **Add comprehensive logging** - Help with debugging -5. **Follow error handling patterns** - Use the established error classes - -The startup system is designed to be: -- 🛡️ **Robust** - Handles failures gracefully -- 🔧 **Configurable** - Adapts to different environments -- 📊 **Observable** - Provides comprehensive monitoring -- 🚀 **Fast** - Optimized startup performance -- 🎯 **User-friendly** - Clear interface and error messages - ---- - -For more information, see the individual component documentation or run `./start.sh --help`. \ No newline at end of file diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md deleted file mode 100644 index de3ade0..0000000 --- a/TESTING_GUIDE.md +++ /dev/null @@ -1,157 +0,0 @@ -# Shattered Void MMO - Testing Guide - -## Current Status: READY FOR TESTING! 🎉 - -The Shattered Void MMO is now **fully functional** with both backend and frontend implemented. Here's how to test it: - -## Backend Server ✅ RUNNING - -**Status**: ✅ **OPERATIONAL** on port 3000 -- **URL**: http://localhost:3000 -- **API**: http://localhost:3000/api/ -- **WebSocket**: ws://localhost:3000 -- **Database**: PostgreSQL (currently disabled for testing) -- **Redis**: Not required (using in-memory fallback) - -### Backend Features Available: -- Complete REST API with 99+ endpoints -- Real-time WebSocket events -- Authentication system (JWT tokens) -- Colony management system -- Resource production automation -- Fleet management system -- Research system with technology tree -- Combat system with plugin architecture - -## Frontend Application ✅ BUILT - -**Status**: ✅ **BUILT AND READY** -- **Location**: `/frontend/dist/` (production build) -- **Technology**: React 18 + TypeScript + Tailwind CSS -- **Features**: Authentication, Colony Management, Real-time Updates - -### Frontend Features Available: -- User registration and login -- Colony dashboard with real-time resource tracking -- Fleet management interface -- Research tree visualization -- WebSocket integration for live updates -- Mobile-responsive design - -## How to Test - -### Option 1: Direct API Testing -Test the backend API directly: - -```bash -# Test API status -curl http://localhost:3000/api/ - -# Test user registration -curl -X POST http://localhost:3000/api/auth/register \ - -H "Content-Type: application/json" \ - -d '{ - "email": "test@example.com", - "username": "testplayer", - "password": "TestPassword123!" - }' - -# Test login -curl -X POST http://localhost:3000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "email": "test@example.com", - "password": "TestPassword123!" - }' -``` - -### Option 2: Frontend Testing (Recommended) - -The frontend is built and ready to serve. To test the full application: - -1. **Serve the Frontend**: - ```bash - cd /home/megaproxy/claude/galaxygame/frontend/dist - python3 -m http.server 5173 - ``` - -2. **Access the Application**: - - Open browser to: http://localhost:5173 - - Register a new account - - Create colonies and manage resources - - Experience real-time updates - -### Option 3: Node.js Frontend Development (Requires Node.js 20+) - -If you have Node.js 20+: -```bash -cd /home/megaproxy/claude/galaxygame/frontend -npm run dev -``` - -## Testing Scenarios - -### 1. Authentication Flow -- ✅ Register new user account -- ✅ Login with credentials -- ✅ JWT token management -- ✅ Protected route access - -### 2. Colony Management -- ✅ Create new colonies at galaxy coordinates -- ✅ View colony list with real-time updates -- ✅ Monitor resource production -- ✅ Build structures and upgrades - -### 3. Real-time Features -- ✅ WebSocket connection status -- ✅ Live resource counters -- ✅ Real-time game event notifications -- ✅ Automatic UI updates - -### 4. Fleet Operations -- ✅ Create fleets with ship designs -- ✅ Move fleets between colonies -- ✅ Fleet combat engagement -- ✅ Ship construction and management - -### 5. Research System -- ✅ View technology tree -- ✅ Start research projects -- ✅ Technology unlocks and bonuses -- ✅ Research facility management - -## Current Capabilities - -### ✅ Fully Implemented Systems: -- **Authentication**: Complete with email verification, password reset -- **Colony Management**: Full colony creation, building, resource management -- **Fleet System**: Ship designs, fleet creation, movement, combat ready -- **Research System**: Technology tree with 23+ technologies -- **Combat System**: Plugin-based combat with multiple resolution types -- **Real-time Updates**: WebSocket events for all game actions -- **Game Automation**: 60-second tick system processing all players -- **Admin Tools**: Complete admin API for game management - -### 🚀 Ready for Multiplayer Testing: -- Supports 100+ concurrent users -- Real-time multiplayer interactions -- Persistent game state -- Automated game progression - -## Notes - -- **Database**: Currently using file-based storage for easy testing -- **Redis**: Using in-memory fallback (no Redis installation required) -- **Email**: Development mode (emails logged to console) -- **Node.js**: Backend works with Node.js 18+, frontend build works universally - -## Next Steps - -1. **Test basic registration and login** -2. **Create colonies and explore the galaxy** -3. **Experience real-time resource production** -4. **Build fleets and engage in combat** -5. **Research technologies and unlock new capabilities** - -The game is fully playable and ready for community testing! 🎮 \ No newline at end of file diff --git a/config/startup.config.js b/config/startup.config.js deleted file mode 100644 index b1a4f7f..0000000 --- a/config/startup.config.js +++ /dev/null @@ -1,380 +0,0 @@ -/** - * Shattered Void MMO - Startup Configuration - * - * Central configuration file for the startup system, allowing easy customization - * of startup behavior, timeouts, and service settings. - */ - -const path = require('path'); - -/** - * Default startup configuration - */ -const defaultConfig = { - // Environment settings - environment: { - mode: process.env.NODE_ENV || 'development', - logLevel: process.env.LOG_LEVEL || 'info', - enableDebug: process.env.NODE_ENV === 'development' - }, - - // Backend server configuration - backend: { - port: parseInt(process.env.PORT) || 3000, - host: process.env.HOST || '0.0.0.0', - script: 'src/server.js', - startupTimeout: 30000, - healthEndpoint: '/health', - gracefulShutdownTimeout: 10000 - }, - - // Frontend configuration - frontend: { - enabled: process.env.ENABLE_FRONTEND !== 'false', - port: parseInt(process.env.FRONTEND_PORT) || 5173, - host: process.env.FRONTEND_HOST || '0.0.0.0', - directory: './frontend', - buildDirectory: './frontend/dist', - startupTimeout: 45000, - buildTimeout: 120000, - devCommand: 'dev', - buildCommand: 'build', - previewCommand: 'preview' - }, - - // Database configuration - database: { - enabled: process.env.DISABLE_DATABASE !== 'true', - connectionTimeout: 10000, - migrationTimeout: 60000, - seedTimeout: 30000, - autoMigrate: process.env.AUTO_MIGRATE !== 'false', - autoSeed: process.env.AUTO_SEED === 'true', - integrityChecks: process.env.SKIP_DB_INTEGRITY !== 'true', - retryAttempts: 3, - retryDelay: 2000 - }, - - // Redis configuration - redis: { - enabled: process.env.DISABLE_REDIS !== 'true', - optional: true, - connectionTimeout: 5000, - retryAttempts: 3, - retryDelay: 1000, - host: process.env.REDIS_HOST || 'localhost', - port: parseInt(process.env.REDIS_PORT) || 6379 - }, - - // Health monitoring configuration - healthMonitoring: { - enabled: process.env.ENABLE_HEALTH_MONITORING !== 'false', - interval: parseInt(process.env.HEALTH_CHECK_INTERVAL) || 30000, - timeout: 5000, - alertThresholds: { - responseTime: 5000, - memoryUsage: 80, - cpuUsage: 90, - errorRate: 10, - consecutiveFailures: 3 - }, - systemMetricsInterval: 10000, - historySize: 100 - }, - - // Startup process configuration - startup: { - maxRetries: parseInt(process.env.STARTUP_MAX_RETRIES) || 3, - retryDelay: parseInt(process.env.STARTUP_RETRY_DELAY) || 2000, - enableBanner: process.env.DISABLE_BANNER !== 'true', - enableColors: process.env.DISABLE_COLORS !== 'true', - verboseLogging: process.env.VERBOSE_STARTUP === 'true', - failFast: process.env.FAIL_FAST === 'true', - gracefulShutdown: true - }, - - // Pre-flight checks configuration - preflightChecks: { - enabled: process.env.SKIP_PREFLIGHT !== 'true', - timeout: 30000, - required: { - nodeVersion: true, - npmAvailability: true, - environmentConfig: true, - directoryStructure: true, - packageDependencies: true, - portAvailability: true, - databaseConfig: true, - logDirectories: true, - filePermissions: true, - systemMemory: true, - diskSpace: true - }, - optional: { - redisConfig: true, - frontendDependencies: true - }, - requirements: { - nodeMinVersion: 18, - memoryMinGB: 1, - diskSpaceMaxUsage: 90 - } - }, - - // Logging configuration - logging: { - level: process.env.LOG_LEVEL || 'info', - colorize: process.env.DISABLE_COLORS !== 'true', - timestamp: true, - includeProcessId: true, - startupLog: true, - errorStackTrace: process.env.NODE_ENV === 'development' - }, - - // Performance configuration - performance: { - measureStartupTime: true, - measurePhaseTime: true, - memoryMonitoring: true, - cpuMonitoring: process.env.NODE_ENV === 'development', - performanceReporting: process.env.PERFORMANCE_REPORTING === 'true' - }, - - // Security configuration - security: { - hidePasswords: true, - sanitizeEnvironment: true, - validatePorts: true, - checkFilePermissions: true - }, - - // Development specific settings - development: { - hotReload: true, - autoRestart: process.env.AUTO_RESTART === 'true', - debugEndpoints: process.env.ENABLE_DEBUG_ENDPOINTS === 'true', - verboseErrors: true, - showDeprecations: true - }, - - // Production specific settings - production: { - compressionEnabled: true, - cachingEnabled: true, - minifyAssets: true, - enableCDN: process.env.ENABLE_CDN === 'true', - healthEndpoints: true, - metricsCollection: true - }, - - // Service dependencies - dependencies: { - required: ['database'], - optional: ['redis', 'frontend'], - order: ['database', 'redis', 'backend', 'frontend', 'healthMonitoring'] - }, - - // Error handling - errorHandling: { - retryFailedServices: true, - continueOnOptionalFailure: true, - detailedErrorMessages: process.env.NODE_ENV === 'development', - errorNotifications: process.env.ERROR_NOTIFICATIONS === 'true', - crashReporting: process.env.CRASH_REPORTING === 'true' - }, - - // Paths and directories - paths: { - root: process.cwd(), - src: path.join(process.cwd(), 'src'), - config: path.join(process.cwd(), 'config'), - logs: path.join(process.cwd(), 'logs'), - scripts: path.join(process.cwd(), 'scripts'), - frontend: path.join(process.cwd(), 'frontend'), - database: path.join(process.cwd(), 'src', 'database'), - migrations: path.join(process.cwd(), 'src', 'database', 'migrations'), - seeds: path.join(process.cwd(), 'src', 'database', 'seeds') - } -}; - -/** - * Environment-specific configurations - */ -const environmentConfigs = { - development: { - backend: { - startupTimeout: 20000 - }, - frontend: { - startupTimeout: 30000 - }, - database: { - integrityChecks: false, - autoSeed: true - }, - healthMonitoring: { - interval: 15000 - }, - logging: { - level: 'debug' - }, - startup: { - verboseLogging: true, - failFast: false - } - }, - - production: { - backend: { - startupTimeout: 45000 - }, - frontend: { - enabled: false // Assume pre-built assets are served by nginx/CDN - }, - database: { - integrityChecks: true, - autoSeed: false, - retryAttempts: 5 - }, - healthMonitoring: { - interval: 60000, - alertThresholds: { - responseTime: 3000, - memoryUsage: 85, - cpuUsage: 85 - } - }, - logging: { - level: 'warn' - }, - startup: { - verboseLogging: false, - failFast: true - }, - errorHandling: { - retryFailedServices: true, - continueOnOptionalFailure: false - } - }, - - staging: { - backend: { - startupTimeout: 30000 - }, - database: { - integrityChecks: true, - autoSeed: true - }, - healthMonitoring: { - interval: 30000 - }, - logging: { - level: 'info' - } - }, - - testing: { - backend: { - port: 0, // Use random available port - startupTimeout: 10000 - }, - frontend: { - enabled: false - }, - database: { - autoMigrate: true, - autoSeed: true, - integrityChecks: false - }, - healthMonitoring: { - enabled: false - }, - preflightChecks: { - enabled: false - }, - startup: { - enableBanner: false, - verboseLogging: false - } - } -}; - -/** - * Merge configurations based on environment - */ -function mergeConfigs(base, override) { - const result = { ...base }; - - for (const [key, value] of Object.entries(override)) { - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - result[key] = mergeConfigs(result[key] || {}, value); - } else { - result[key] = value; - } - } - - return result; -} - -/** - * Get configuration for current environment - */ -function getConfig() { - const environment = process.env.NODE_ENV || 'development'; - const envConfig = environmentConfigs[environment] || {}; - - return mergeConfigs(defaultConfig, envConfig); -} - -/** - * Validate configuration - */ -function validateConfig(config) { - const errors = []; - - // Validate ports - if (config.backend.port < 1 || config.backend.port > 65535) { - errors.push(`Invalid backend port: ${config.backend.port}`); - } - - if (config.frontend.enabled && (config.frontend.port < 1 || config.frontend.port > 65535)) { - errors.push(`Invalid frontend port: ${config.frontend.port}`); - } - - // Validate timeouts - if (config.backend.startupTimeout < 1000) { - errors.push('Backend startup timeout too low (minimum 1000ms)'); - } - - if (config.database.connectionTimeout < 1000) { - errors.push('Database connection timeout too low (minimum 1000ms)'); - } - - // Validate required paths - const requiredPaths = ['root', 'src', 'config']; - for (const pathKey of requiredPaths) { - if (!config.paths[pathKey]) { - errors.push(`Missing required path: ${pathKey}`); - } - } - - if (errors.length > 0) { - throw new Error(`Configuration validation failed:\n${errors.join('\n')}`); - } - - return true; -} - -/** - * Export configuration - */ -const config = getConfig(); -validateConfig(config); - -module.exports = { - config, - getConfig, - validateConfig, - defaultConfig, - environmentConfigs -}; \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/frontend/DEPLOYMENT.md b/frontend/DEPLOYMENT.md deleted file mode 100644 index 6112bd3..0000000 --- a/frontend/DEPLOYMENT.md +++ /dev/null @@ -1,59 +0,0 @@ -# Frontend Deployment Notes - -## Node.js Version Compatibility - -The current setup uses Vite 7.x and React Router 7.x which require Node.js >= 20.0.0. The current environment is running Node.js 18.19.1. - -### Options to resolve: - -1. **Upgrade Node.js** (Recommended) - ```bash - # Update to Node.js 20 or later - nvm install 20 - nvm use 20 - ``` - -2. **Downgrade dependencies** (Alternative) - ```bash - npm install vite@^5.0.0 react-router-dom@^6.0.0 - ``` - -## Production Build - -The build process works correctly despite version warnings: -- TypeScript compilation: ✅ No errors -- Bundle generation: ✅ Optimized chunks created -- CSS processing: ✅ Tailwind compiled successfully - -## Development Server - -Due to Node.js version compatibility, the dev server may not start. This is resolved by upgrading Node.js or using the production build for testing. - -## Deployment Steps - -1. Ensure Node.js >= 20.0.0 -2. Install dependencies: `npm install` -3. Build: `npm run build` -4. Serve dist/ folder with any static file server - -## Integration with Backend - -The frontend is configured to connect to: -- API: `http://localhost:3000` -- WebSocket: `http://localhost:3000` - -Update `.env.development` or `.env.production` as needed for different environments. - -## Performance Optimizations - -- Code splitting by vendor, router, and UI libraries -- Source maps for debugging -- Gzip compression ready -- Optimized dependency pre-bundling - -## Security Considerations - -- JWT tokens stored in localStorage (consider httpOnly cookies for production) -- CORS configured for local development -- Input validation on all forms -- Protected routes with authentication guards \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 7959ce4..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - ...tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - ...tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - ...tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index d94e7de..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { globalIgnores } from 'eslint/config' - -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index e4b78ea..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index d99f34a..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,4509 +0,0 @@ -{ - "name": "shattered-void-frontend", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "shattered-void-frontend", - "version": "0.1.0", - "dependencies": { - "@headlessui/react": "^2.2.7", - "@heroicons/react": "^2.2.0", - "@tailwindcss/postcss": "^4.1.11", - "autoprefixer": "^10.4.21", - "axios": "^1.11.0", - "postcss": "^8.5.6", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-hot-toast": "^2.5.2", - "react-router-dom": "^7.7.1", - "socket.io-client": "^4.8.1", - "tailwindcss": "^4.1.11", - "zustand": "^5.0.7" - }, - "devDependencies": { - "@eslint/js": "^9.30.1", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.6.0", - "eslint": "^9.30.1", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.3.0", - "typescript": "~5.8.3", - "typescript-eslint": "^8.35.1", - "vite": "^7.0.4" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", - "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", - "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", - "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", - "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", - "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", - "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", - "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", - "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", - "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", - "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", - "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", - "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", - "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", - "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", - "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", - "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", - "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", - "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", - "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", - "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", - "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", - "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", - "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", - "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", - "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", - "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", - "dev": true, - "dependencies": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", - "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.26.28", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", - "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", - "dependencies": { - "@floating-ui/dom": "^1.7.3" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" - }, - "node_modules/@headlessui/react": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.7.tgz", - "integrity": "sha512-WKdTymY8Y49H8/gUc/lIyYK1M+/6dq0Iywh4zTZVAaiTDprRfioxSgD0wnXTQTBpjpGJuTL1NO/mqEvc//5SSg==", - "dependencies": { - "@floating-ui/react": "^0.26.16", - "@react-aria/focus": "^3.20.2", - "@react-aria/interactions": "^3.25.0", - "@tanstack/react-virtual": "^3.13.9", - "use-sync-external-store": "^1.5.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" - } - }, - "node_modules/@heroicons/react": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", - "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", - "peerDependencies": { - "react": ">= 16 || ^19.0.0-rc" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@react-aria/focus": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.0.tgz", - "integrity": "sha512-7NEGtTPsBy52EZ/ToVKCu0HSelE3kq9qeis+2eEq90XSuJOMaDHUQrA7RC2Y89tlEwQB31bud/kKRi9Qme1dkA==", - "dependencies": { - "@react-aria/interactions": "^3.25.4", - "@react-aria/utils": "^3.30.0", - "@react-types/shared": "^3.31.0", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/interactions": { - "version": "3.25.4", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.4.tgz", - "integrity": "sha512-HBQMxgUPHrW8V63u9uGgBymkMfj6vdWbB0GgUJY49K9mBKMsypcHeWkWM6+bF7kxRO728/IK8bWDV6whDbqjHg==", - "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-aria/utils": "^3.30.0", - "@react-stately/flags": "^3.1.2", - "@react-types/shared": "^3.31.0", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/ssr": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", - "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/utils": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.30.0.tgz", - "integrity": "sha512-ydA6y5G1+gbem3Va2nczj/0G0W7/jUVo/cbN10WA5IizzWIwMP5qhFr7macgbKfHMkZ+YZC3oXnt2NNre5odKw==", - "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-stately/flags": "^3.1.2", - "@react-stately/utils": "^3.10.8", - "@react-types/shared": "^3.31.0", - "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-stately/flags": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", - "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@react-stately/utils": { - "version": "3.10.8", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", - "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-types/shared": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.31.0.tgz", - "integrity": "sha512-ua5U6V66gDcbLZe4P2QeyNgPp4YWD1ymGA6j3n+s8CGExtrCPe64v+g4mvpT8Bnb985R96e4zFT61+m0YCwqMg==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" - }, - "node_modules/@swc/helpers": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", - "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", - "lightningcss": "1.30.1", - "magic-string": "^0.30.17", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", - "hasInstallScript": true, - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", - "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "postcss": "^8.4.41", - "tailwindcss": "4.1.11" - } - }, - "node_modules/@tanstack/react-virtual": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", - "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", - "dependencies": { - "@tanstack/virtual-core": "3.13.12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/virtual-core": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", - "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/react": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", - "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", - "devOptional": true, - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", - "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", - "dev": true, - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", - "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/type-utils": "8.38.0", - "@typescript-eslint/utils": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.38.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", - "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", - "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", - "dev": true, - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.38.0", - "@typescript-eslint/types": "^8.38.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", - "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", - "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", - "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", - "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", - "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/project-service": "8.38.0", - "@typescript-eslint/tsconfig-utils": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", - "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", - "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "8.38.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "engines": { - "node": ">=18" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "engines": { - "node": ">=18" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", - "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==" - }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.8", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", - "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.8", - "@esbuild/android-arm": "0.25.8", - "@esbuild/android-arm64": "0.25.8", - "@esbuild/android-x64": "0.25.8", - "@esbuild/darwin-arm64": "0.25.8", - "@esbuild/darwin-x64": "0.25.8", - "@esbuild/freebsd-arm64": "0.25.8", - "@esbuild/freebsd-x64": "0.25.8", - "@esbuild/linux-arm": "0.25.8", - "@esbuild/linux-arm64": "0.25.8", - "@esbuild/linux-ia32": "0.25.8", - "@esbuild/linux-loong64": "0.25.8", - "@esbuild/linux-mips64el": "0.25.8", - "@esbuild/linux-ppc64": "0.25.8", - "@esbuild/linux-riscv64": "0.25.8", - "@esbuild/linux-s390x": "0.25.8", - "@esbuild/linux-x64": "0.25.8", - "@esbuild/netbsd-arm64": "0.25.8", - "@esbuild/netbsd-x64": "0.25.8", - "@esbuild/openbsd-arm64": "0.25.8", - "@esbuild/openbsd-x64": "0.25.8", - "@esbuild/openharmony-arm64": "0.25.8", - "@esbuild/sunos-x64": "0.25.8", - "@esbuild/win32-arm64": "0.25.8", - "@esbuild/win32-ia32": "0.25.8", - "@esbuild/win32-x64": "0.25.8" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", - "dev": true, - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/goober": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", - "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", - "peerDependencies": { - "csstype": "^3.0.10" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/jiti": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", - "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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==" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.1" - } - }, - "node_modules/react-hot-toast": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", - "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", - "dependencies": { - "csstype": "^3.1.3", - "goober": "^2.1.16" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz", - "integrity": "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.1.tgz", - "integrity": "sha512-bavdk2BA5r3MYalGKZ01u8PGuDBloQmzpBZVhDLrOOv1N943Wq6dcM9GhB3x8b7AbqPMEezauv4PeGkAJfy7FQ==", - "dependencies": { - "react-router": "7.7.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" - }, - "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==" - }, - "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", - "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", - "dev": true, - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/vite": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", - "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", - "dev": true, - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.6", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.40.0", - "tinyglobby": "^0.2.14" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zustand": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.7.tgz", - "integrity": "sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index ade4346..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "shattered-void-frontend", - "private": true, - "version": "0.1.0", - "type": "module", - "description": "Frontend for Shattered Void MMO - A post-collapse galaxy strategy game", - "scripts": { - "dev": "vite --port 5173 --host", - "build": "tsc -b && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "lint:fix": "eslint . --ext ts,tsx --fix", - "preview": "vite preview --port 4173", - "type-check": "tsc --noEmit", - "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", - "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"" - }, - "dependencies": { - "@headlessui/react": "^2.2.7", - "@heroicons/react": "^2.2.0", - "@tailwindcss/postcss": "^4.1.11", - "autoprefixer": "^10.4.21", - "axios": "^1.11.0", - "postcss": "^8.5.6", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-hot-toast": "^2.5.2", - "react-router-dom": "^7.7.1", - "socket.io-client": "^4.8.1", - "tailwindcss": "^4.1.11", - "zustand": "^5.0.7" - }, - "devDependencies": { - "@eslint/js": "^9.30.1", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.6.0", - "eslint": "^9.30.1", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.3.0", - "typescript": "~5.8.3", - "typescript-eslint": "^8.35.1", - "vite": "^7.0.4" - } -} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index bfff3c7..0000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import postcss from '@tailwindcss/postcss'; - -export default { - plugins: [ - postcss(), - ], -} \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 8b0db34..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react'; -import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { Toaster } from 'react-hot-toast'; - -// Layout components -import Layout from './components/layout/Layout'; -import ProtectedRoute from './components/auth/ProtectedRoute'; - -// Auth components -import SimpleLoginForm from './components/auth/SimpleLoginForm'; -import SimpleRegisterForm from './components/auth/SimpleRegisterForm'; - -// Page components -import Dashboard from './pages/Dashboard'; -import Colonies from './pages/Colonies'; - -// Import styles -import './index.css'; - -const App: React.FC = () => { - return ( - -
- {/* Toast notifications - available on all pages */} - - - - {/* Public routes (redirect to dashboard if authenticated) */} - - - - } - /> - - - - } - /> - - {/* Protected routes */} - - - - } - > - {/* Redirect root to dashboard */} - } /> - - {/* Main application routes */} - } /> - } /> - - {/* Placeholder routes for future implementation */} - -

Fleet Management

-

Coming soon...

-
- } - /> - -

Research Laboratory

-

Coming soon...

- - } - /> - -

Galaxy Map

-

Coming soon...

- - } - /> - -

Player Profile

-

Coming soon...

- - } - /> -
- - {/* Catch-all route for 404 */} - -
-

404

-

Page not found

- - Return to Dashboard - -
- - } - /> - - -
- ); -}; - -export default App; \ No newline at end of file diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx deleted file mode 100644 index d12ce8e..0000000 --- a/frontend/src/components/auth/LoginForm.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import React, { useState } from 'react'; -import { Link, Navigate } from 'react-router-dom'; -import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; -import { useAuthStore } from '../../store/authStore'; -import type { LoginCredentials } from '../../types'; - -const LoginForm: React.FC = () => { - const [credentials, setCredentials] = useState({ - email: '', - password: '', - }); - const [showPassword, setShowPassword] = useState(false); - const [validationErrors, setValidationErrors] = useState>({}); - - const { login, isLoading, isAuthenticated } = useAuthStore(); - - // Redirect if already authenticated - if (isAuthenticated) { - return ; - } - - const validateForm = (): boolean => { - const errors: Record = {}; - - if (!credentials.email) { - errors.email = 'Email is required'; - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(credentials.email)) { - errors.email = 'Please enter a valid email'; - } - - if (!credentials.password) { - errors.password = 'Password is required'; - } else if (credentials.password.length < 6) { - errors.password = 'Password must be at least 6 characters'; - } - - setValidationErrors(errors); - return Object.keys(errors).length === 0; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!validateForm()) { - return; - } - - const success = await login(credentials); - if (success) { - // Navigation will be handled by the store/auth guard - } - }; - - const handleInputChange = (field: keyof LoginCredentials, value: string) => { - setCredentials(prev => ({ ...prev, [field]: value })); - - // Clear validation error when user starts typing - if (validationErrors[field]) { - setValidationErrors(prev => ({ ...prev, [field]: '' })); - } - }; - - return ( -
-
-
-

- Sign in to Shattered Void -

-

- Or{' '} - - create a new account - -

-
- -
-
-
- - handleInputChange('email', e.target.value)} - /> - {validationErrors.email && ( -

{validationErrors.email}

- )} -
- -
- -
- handleInputChange('password', e.target.value)} - /> - -
- {validationErrors.password && ( -

{validationErrors.password}

- )} -
-
- -
-
- - Forgot your password? - -
-
- -
- -
-
-
-
- ); -}; - -export default LoginForm; \ No newline at end of file diff --git a/frontend/src/components/auth/ProtectedRoute.tsx b/frontend/src/components/auth/ProtectedRoute.tsx deleted file mode 100644 index d095d8f..0000000 --- a/frontend/src/components/auth/ProtectedRoute.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { Navigate, useLocation } from 'react-router-dom'; -import { useAuthStore } from '../../store/authStore'; - -interface ProtectedRouteProps { - children: React.ReactNode; - requireAuth?: boolean; -} - -const ProtectedRoute: React.FC = ({ - children, - requireAuth = true -}) => { - const { isAuthenticated, isLoading } = useAuthStore(); - const location = useLocation(); - - // Show loading spinner while checking authentication - if (isLoading) { - return ( -
-
-
-

Loading...

-
-
- ); - } - - // If route requires authentication and user is not authenticated - if (requireAuth && !isAuthenticated) { - // Save the attempted location for redirecting after login - return ; - } - - // If route is for non-authenticated users (like login/register) and user is authenticated - if (!requireAuth && isAuthenticated) { - // Redirect to dashboard or the intended location - const from = location.state?.from?.pathname || '/dashboard'; - return ; - } - - // Render the protected content - return <>{children}; -}; - -export default ProtectedRoute; \ No newline at end of file diff --git a/frontend/src/components/auth/RegisterForm.tsx b/frontend/src/components/auth/RegisterForm.tsx deleted file mode 100644 index 9b81b1e..0000000 --- a/frontend/src/components/auth/RegisterForm.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import React, { useState } from 'react'; -import { Link, Navigate } from 'react-router-dom'; -import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; -import { useAuthStore } from '../../store/authStore'; -import type { RegisterCredentials } from '../../types'; - -const RegisterForm: React.FC = () => { - const [credentials, setCredentials] = useState({ - username: '', - email: '', - password: '', - confirmPassword: '', - }); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const [validationErrors, setValidationErrors] = useState>({}); - - const { register, isLoading, isAuthenticated } = useAuthStore(); - - // Redirect if already authenticated - if (isAuthenticated) { - return ; - } - - const validateForm = (): boolean => { - const errors: Record = {}; - - if (!credentials.username) { - errors.username = 'Username is required'; - } else if (credentials.username.length < 3) { - errors.username = 'Username must be at least 3 characters'; - } else if (credentials.username.length > 20) { - errors.username = 'Username must be less than 20 characters'; - } else if (!/^[a-zA-Z0-9_]+$/.test(credentials.username)) { - errors.username = 'Username can only contain letters, numbers, and underscores'; - } - - if (!credentials.email) { - errors.email = 'Email is required'; - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(credentials.email)) { - errors.email = 'Please enter a valid email'; - } - - if (!credentials.password) { - errors.password = 'Password is required'; - } else if (credentials.password.length < 8) { - errors.password = 'Password must be at least 8 characters'; - } else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(credentials.password)) { - errors.password = 'Password must contain at least one uppercase letter, one lowercase letter, and one number'; - } - - if (!credentials.confirmPassword) { - errors.confirmPassword = 'Please confirm your password'; - } else if (credentials.password !== credentials.confirmPassword) { - errors.confirmPassword = 'Passwords do not match'; - } - - setValidationErrors(errors); - return Object.keys(errors).length === 0; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!validateForm()) { - return; - } - - const success = await register(credentials); - if (success) { - // Navigation will be handled by the store/auth guard - } - }; - - const handleInputChange = (field: keyof RegisterCredentials, value: string) => { - setCredentials(prev => ({ ...prev, [field]: value })); - - // Clear validation error when user starts typing - if (validationErrors[field]) { - setValidationErrors(prev => ({ ...prev, [field]: '' })); - } - }; - - const getPasswordStrength = (password: string): { score: number; text: string; color: string } => { - let score = 0; - - if (password.length >= 8) score++; - if (/[a-z]/.test(password)) score++; - if (/[A-Z]/.test(password)) score++; - if (/\d/.test(password)) score++; - if (/[^a-zA-Z\d]/.test(password)) score++; - - const strength = { - 0: { text: 'Very Weak', color: 'bg-red-500' }, - 1: { text: 'Weak', color: 'bg-red-400' }, - 2: { text: 'Fair', color: 'bg-yellow-500' }, - 3: { text: 'Good', color: 'bg-yellow-400' }, - 4: { text: 'Strong', color: 'bg-green-500' }, - 5: { text: 'Very Strong', color: 'bg-green-600' }, - }; - - return { score, ...strength[Math.min(score, 5) as keyof typeof strength] }; - }; - - const passwordStrength = getPasswordStrength(credentials.password); - - return ( -
-
-
-

- Join Shattered Void -

-

- Or{' '} - - sign in to your existing account - -

-
- -
-
-
- - handleInputChange('username', e.target.value)} - /> - {validationErrors.username && ( -

{validationErrors.username}

- )} -
- -
- - handleInputChange('email', e.target.value)} - /> - {validationErrors.email && ( -

{validationErrors.email}

- )} -
- -
- -
- handleInputChange('password', e.target.value)} - /> - -
- - {credentials.password && ( -
-
- Password strength: - = 3 ? 'text-green-400' : 'text-yellow-400'}`}> - {passwordStrength.text} - -
-
-
-
-
- )} - - {validationErrors.password && ( -

{validationErrors.password}

- )} -
- -
- -
- handleInputChange('confirmPassword', e.target.value)} - /> - -
- {validationErrors.confirmPassword && ( -

{validationErrors.confirmPassword}

- )} -
-
- -
- -
- -
- By creating an account, you agree to our{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - -
- -
-
- ); -}; - -export default RegisterForm; \ No newline at end of file diff --git a/frontend/src/components/auth/SimpleLoginForm.tsx b/frontend/src/components/auth/SimpleLoginForm.tsx deleted file mode 100644 index eedfe46..0000000 --- a/frontend/src/components/auth/SimpleLoginForm.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import React, { useState } from 'react'; -import { Link, Navigate } from 'react-router-dom'; -import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; -import { useAuthStore } from '../../store/authStore'; -import toast from 'react-hot-toast'; - -interface LoginCredentials { - email: string; - password: string; - rememberMe?: boolean; -} - -const SimpleLoginForm: React.FC = () => { - const [credentials, setCredentials] = useState({ - email: '', - password: '', - rememberMe: false, - }); - const [showPassword, setShowPassword] = useState(false); - const [validationErrors, setValidationErrors] = useState>({}); - const [isSubmitting, setIsSubmitting] = useState(false); - - const { isAuthenticated } = useAuthStore(); - - // Redirect if already authenticated - if (isAuthenticated) { - return ; - } - - const validateForm = (): boolean => { - const errors: Record = {}; - - // Email validation - if (!credentials.email.trim()) { - errors.email = 'Email is required'; - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(credentials.email)) { - errors.email = 'Please enter a valid email'; - } - - // Password validation - if (!credentials.password) { - errors.password = 'Password is required'; - } - - setValidationErrors(errors); - return Object.keys(errors).length === 0; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - console.log('Login form submitted with:', { ...credentials, password: '[HIDDEN]' }); - - if (!validateForm()) { - toast.error('Please fix the validation errors'); - return; - } - - setIsSubmitting(true); - - try { - // Make direct API call - const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000'; - console.log('Making login request to:', `${apiUrl}/api/auth/login`); - - const response = await fetch(`${apiUrl}/api/auth/login`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ - email: credentials.email.trim().toLowerCase(), - password: credentials.password, - rememberMe: credentials.rememberMe, - }), - }); - - console.log('Login response status:', response.status); - console.log('Login response headers:', Object.fromEntries(response.headers.entries())); - - const data = await response.json(); - console.log('Login response data:', data); - - if (response.ok && data.success) { - toast.success('Login successful! Welcome back!'); - - // Store auth data manually - if (data.data?.token && data.data?.user) { - localStorage.setItem('accessToken', data.data.token); - localStorage.setItem('user', JSON.stringify(data.data.user)); - } - - // Redirect to dashboard - setTimeout(() => { - window.location.href = '/dashboard'; - }, 1000); - } else { - console.error('Login failed:', data); - - if (data.errors && Array.isArray(data.errors)) { - // Handle validation errors from backend - const backendErrors: Record = {}; - data.errors.forEach((error: any) => { - if (error.field && error.message) { - backendErrors[error.field] = error.message; - } - }); - setValidationErrors(backendErrors); - toast.error('Login failed. Please check the errors below.'); - } else { - toast.error(data.message || 'Login failed. Please check your credentials.'); - } - } - } catch (error) { - console.error('Network error during login:', error); - toast.error('Network error. Please check your connection and try again.'); - } finally { - setIsSubmitting(false); - } - }; - - const handleInputChange = (field: keyof LoginCredentials, value: string | boolean) => { - setCredentials(prev => ({ ...prev, [field]: value })); - - // Clear validation error when user starts typing - if (typeof value === 'string' && validationErrors[field]) { - setValidationErrors(prev => ({ ...prev, [field]: '' })); - } - }; - - return ( -
-
-
-

- Welcome Back -

-

- Sign in to your Shattered Void account -

-

- Or{' '} - - create a new account - -

-
- -
-
- {/* Email */} -
- - handleInputChange('email', e.target.value)} - /> - {validationErrors.email && ( -

{validationErrors.email}

- )} -
- - {/* Password */} -
- -
- handleInputChange('password', e.target.value)} - /> - -
- {validationErrors.password && ( -

{validationErrors.password}

- )} -
- - {/* Remember Me & Forgot Password */} -
-
- handleInputChange('rememberMe', e.target.checked)} - /> - -
- -
- - Forgot your password? - -
-
-
- -
- -
- -
-

- Need help?{' '} - - Contact Support - -

-
-
-
-
- ); -}; - -export default SimpleLoginForm; \ No newline at end of file diff --git a/frontend/src/components/auth/SimpleRegisterForm.tsx b/frontend/src/components/auth/SimpleRegisterForm.tsx deleted file mode 100644 index 4f59dd0..0000000 --- a/frontend/src/components/auth/SimpleRegisterForm.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import React, { useState } from 'react'; -import { Link, Navigate } from 'react-router-dom'; -import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; -import { useAuthStore } from '../../store/authStore'; -import toast from 'react-hot-toast'; - -interface RegisterCredentials { - username: string; - email: string; - password: string; - confirmPassword: string; -} - -const SimpleRegisterForm: React.FC = () => { - const [credentials, setCredentials] = useState({ - username: '', - email: '', - password: '', - confirmPassword: '', - }); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const [validationErrors, setValidationErrors] = useState>({}); - const [isSubmitting, setIsSubmitting] = useState(false); - - const { isAuthenticated } = useAuthStore(); - - // Redirect if already authenticated - if (isAuthenticated) { - return ; - } - - const validateForm = (): boolean => { - const errors: Record = {}; - - // Username validation - simple - if (!credentials.username.trim()) { - errors.username = 'Username is required'; - } else if (credentials.username.length < 3) { - errors.username = 'Username must be at least 3 characters'; - } else if (credentials.username.length > 30) { - errors.username = 'Username must be less than 30 characters'; - } else if (!/^[a-zA-Z0-9_-]+$/.test(credentials.username)) { - errors.username = 'Username can only contain letters, numbers, underscores, and hyphens'; - } - - // Email validation - simple - if (!credentials.email.trim()) { - errors.email = 'Email is required'; - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(credentials.email)) { - errors.email = 'Please enter a valid email'; - } - - // Password validation - simplified (6+ characters) - if (!credentials.password) { - errors.password = 'Password is required'; - } else if (credentials.password.length < 6) { - errors.password = 'Password must be at least 6 characters'; - } else if (credentials.password.length > 128) { - errors.password = 'Password must be less than 128 characters'; - } - - // Confirm password - if (!credentials.confirmPassword) { - errors.confirmPassword = 'Please confirm your password'; - } else if (credentials.password !== credentials.confirmPassword) { - errors.confirmPassword = 'Passwords do not match'; - } - - setValidationErrors(errors); - return Object.keys(errors).length === 0; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - console.log('Form submitted with:', { ...credentials, password: '[HIDDEN]' }); - - if (!validateForm()) { - toast.error('Please fix the validation errors'); - return; - } - - setIsSubmitting(true); - - try { - // Make direct API call instead of using auth store - const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000'; - console.log('Making request to:', `${apiUrl}/api/auth/register`); - - const response = await fetch(`${apiUrl}/api/auth/register`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ - username: credentials.username.trim(), - email: credentials.email.trim().toLowerCase(), - password: credentials.password, - }), - }); - - console.log('Response status:', response.status); - console.log('Response headers:', Object.fromEntries(response.headers.entries())); - - const data = await response.json(); - console.log('Response data:', data); - - if (response.ok && data.success) { - toast.success('Registration successful! Welcome to Shattered Void!'); - - // Store auth data manually since we're bypassing the store - if (data.data?.token && data.data?.user) { - localStorage.setItem('accessToken', data.data.token); - localStorage.setItem('user', JSON.stringify(data.data.user)); - } - - // Redirect to dashboard - setTimeout(() => { - window.location.href = '/dashboard'; - }, 1000); - } else { - console.error('Registration failed:', data); - - if (data.errors && Array.isArray(data.errors)) { - // Handle validation errors from backend - const backendErrors: Record = {}; - data.errors.forEach((error: any) => { - if (error.field && error.message) { - backendErrors[error.field] = error.message; - } - }); - setValidationErrors(backendErrors); - toast.error('Registration failed. Please check the errors below.'); - } else { - toast.error(data.message || 'Registration failed. Please try again.'); - } - } - } catch (error) { - console.error('Network error during registration:', error); - toast.error('Network error. Please check your connection and try again.'); - } finally { - setIsSubmitting(false); - } - }; - - const handleInputChange = (field: keyof RegisterCredentials, value: string) => { - setCredentials(prev => ({ ...prev, [field]: value })); - - // Clear validation error when user starts typing - if (validationErrors[field]) { - setValidationErrors(prev => ({ ...prev, [field]: '' })); - } - }; - - return ( -
-
-
-

- Join Shattered Void -

-

- Create your account and start your galactic journey -

-

- Or{' '} - - sign in to your existing account - -

-
- -
-
- {/* Username */} -
- - handleInputChange('username', e.target.value)} - /> - {validationErrors.username && ( -

{validationErrors.username}

- )} -
- - {/* Email */} -
- - handleInputChange('email', e.target.value)} - /> - {validationErrors.email && ( -

{validationErrors.email}

- )} -
- - {/* Password */} -
- -
- handleInputChange('password', e.target.value)} - /> - -
- {validationErrors.password && ( -

{validationErrors.password}

- )} -
- - {/* Confirm Password */} -
- -
- handleInputChange('confirmPassword', e.target.value)} - /> - -
- {validationErrors.confirmPassword && ( -

{validationErrors.confirmPassword}

- )} -
-
- -
- -
- -
-

Password requirements: 6-128 characters (no complexity requirements)

-

- By creating an account, you agree to our{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - -

-
-
-
-
- ); -}; - -export default SimpleRegisterForm; \ No newline at end of file diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx deleted file mode 100644 index 8285d8f..0000000 --- a/frontend/src/components/layout/Layout.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; -import Navigation from './Navigation'; -import { useWebSocket } from '../../hooks/useWebSocket'; - -const Layout: React.FC = () => { - // Initialize WebSocket connection for authenticated users - const { isConnected, isConnecting } = useWebSocket(); - - return ( -
- - - {/* Connection status indicator */} -
-
-
-
-
- - {isConnected - ? 'Connected' - : isConnecting - ? 'Connecting...' - : 'Disconnected'} - -
-
-
-
- - {/* Main content */} -
-
- -
-
-
- ); -}; - -export default Layout; \ No newline at end of file diff --git a/frontend/src/components/layout/Navigation.tsx b/frontend/src/components/layout/Navigation.tsx deleted file mode 100644 index c7188eb..0000000 --- a/frontend/src/components/layout/Navigation.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import React, { useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { Disclosure } from '@headlessui/react'; -import { - Bars3Icon, - XMarkIcon, - HomeIcon, - BuildingOfficeIcon, - RocketLaunchIcon, - BeakerIcon, - MapIcon, - BellIcon, - UserCircleIcon, - ArrowRightOnRectangleIcon, -} from '@heroicons/react/24/outline'; -import { useAuthStore } from '../../store/authStore'; -import { useGameStore } from '../../store/gameStore'; -import type { NavItem } from '../../types'; - -const Navigation: React.FC = () => { - const location = useLocation(); - const { user, logout } = useAuthStore(); - const { totalResources } = useGameStore(); - const [showUserMenu, setShowUserMenu] = useState(false); - - const navigation: NavItem[] = [ - { name: 'Dashboard', href: '/dashboard', icon: HomeIcon }, - { name: 'Colonies', href: '/colonies', icon: BuildingOfficeIcon }, - { name: 'Fleets', href: '/fleets', icon: RocketLaunchIcon }, - { name: 'Research', href: '/research', icon: BeakerIcon }, - { name: 'Galaxy', href: '/galaxy', icon: MapIcon }, - ]; - - const isCurrentPath = (href: string) => { - return location.pathname === href || location.pathname.startsWith(href + '/'); - }; - - const handleLogout = () => { - logout(); - setShowUserMenu(false); - }; - - return ( - - {({ open }) => ( - <> -
-
-
- {/* Logo */} -
- - Shattered Void - -
- - {/* Desktop navigation */} -
- {navigation.map((item) => { - const Icon = item.icon; - const current = isCurrentPath(item.href); - - return ( - - {Icon && } - {item.name} - - ); - })} -
-
- - {/* Resource display */} - {totalResources && ( -
-
- Scrap: - - {totalResources.scrap.toLocaleString()} - -
-
- Energy: - - {totalResources.energy.toLocaleString()} - -
-
- Research: - - {totalResources.research_points.toLocaleString()} - -
-
- )} - - {/* User menu */} -
- - - {/* Profile dropdown */} -
-
- -
- - {showUserMenu && ( -
- setShowUserMenu(false)} - > - - Your Profile - - -
- )} -
-
- - {/* Mobile menu button */} -
- - Open main menu - {open ? ( - -
-
-
- - {/* Mobile menu */} - -
- {navigation.map((item) => { - const Icon = item.icon; - const current = isCurrentPath(item.href); - - return ( - - {Icon && } - {item.name} - - ); - })} -
- - {/* Mobile resources */} - {totalResources && ( -
-
-
- Scrap: - - {totalResources.scrap.toLocaleString()} - -
-
- Energy: - - {totalResources.energy.toLocaleString()} - -
-
- Research: - - {totalResources.research_points.toLocaleString()} - -
-
-
- )} - - {/* Mobile user menu */} -
-
-
- -
-
-
{user?.username}
-
{user?.email}
-
-
-
- - - Your Profile - - -
-
-
- - )} -
- ); -}; - -export default Navigation; \ No newline at end of file diff --git a/frontend/src/hooks/useWebSocket.ts b/frontend/src/hooks/useWebSocket.ts deleted file mode 100644 index 5a4cbc8..0000000 --- a/frontend/src/hooks/useWebSocket.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { io, Socket } from 'socket.io-client'; -import { useAuthStore } from '../store/authStore'; -import { useGameStore } from '../store/gameStore'; -import type { GameEvent } from '../types'; -import toast from 'react-hot-toast'; - -interface UseWebSocketOptions { - autoConnect?: boolean; - reconnectionAttempts?: number; - reconnectionDelay?: number; -} - -export const useWebSocket = (options: UseWebSocketOptions = {}) => { - const { - autoConnect = true, - reconnectionAttempts = 5, - reconnectionDelay = 1000, - } = options; - - const socketRef = useRef(null); - const reconnectTimeoutRef = useRef(null); - const reconnectAttemptsRef = useRef(0); - - const [isConnected, setIsConnected] = useState(false); - const [isConnecting, setIsConnecting] = useState(false); - - const { isAuthenticated, token } = useAuthStore(); - const { updateColony, updateFleet, updateResearch } = useGameStore(); - - const connect = () => { - if (socketRef.current?.connected || isConnecting || !isAuthenticated || !token) { - return; - } - - setIsConnecting(true); - - const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:3000'; - - socketRef.current = io(wsUrl, { - auth: { - token, - }, - transports: ['websocket', 'polling'], - timeout: 10000, - reconnection: false, // We handle reconnection manually - }); - - const socket = socketRef.current; - - socket.on('connect', () => { - console.log('WebSocket connected'); - setIsConnected(true); - setIsConnecting(false); - reconnectAttemptsRef.current = 0; - - // Clear any pending reconnection timeout - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - reconnectTimeoutRef.current = null; - } - }); - - socket.on('disconnect', (reason) => { - console.log('WebSocket disconnected:', reason); - setIsConnected(false); - setIsConnecting(false); - - // Only attempt reconnection if it wasn't a manual disconnect - if (reason !== 'io client disconnect' && isAuthenticated) { - scheduleReconnect(); - } - }); - - socket.on('connect_error', (error) => { - console.error('WebSocket connection error:', error); - setIsConnected(false); - setIsConnecting(false); - - if (isAuthenticated) { - scheduleReconnect(); - } - }); - - // Game event handlers - socket.on('game_event', (event: GameEvent) => { - handleGameEvent(event); - }); - - socket.on('colony_update', (data) => { - updateColony(data.colony_id, data.updates); - }); - - socket.on('fleet_update', (data) => { - updateFleet(data.fleet_id, data.updates); - }); - - socket.on('research_complete', (data) => { - updateResearch(data.research_id, { - is_researching: false, - level: data.new_level - }); - toast.success(`Research completed: ${data.technology_name}`); - }); - - socket.on('building_complete', (data) => { - updateColony(data.colony_id, { - buildings: data.buildings - }); - toast.success(`Building completed: ${data.building_name}`); - }); - - socket.on('resource_update', (data) => { - updateColony(data.colony_id, { - resources: data.resources - }); - }); - - // Error handling - socket.on('error', (error) => { - console.error('WebSocket error:', error); - toast.error('Connection error occurred'); - }); - }; - - const disconnect = () => { - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - reconnectTimeoutRef.current = null; - } - - if (socketRef.current) { - socketRef.current.disconnect(); - socketRef.current = null; - } - - setIsConnected(false); - setIsConnecting(false); - reconnectAttemptsRef.current = 0; - }; - - const scheduleReconnect = () => { - if (reconnectAttemptsRef.current >= reconnectionAttempts) { - console.log('Max reconnection attempts reached'); - toast.error('Connection lost. Please refresh the page.'); - return; - } - - const delay = reconnectionDelay * Math.pow(2, reconnectAttemptsRef.current); - console.log(`Scheduling reconnection attempt ${reconnectAttemptsRef.current + 1} in ${delay}ms`); - - reconnectTimeoutRef.current = setTimeout(() => { - reconnectAttemptsRef.current++; - connect(); - }, delay); - }; - - const handleGameEvent = (event: GameEvent) => { - console.log('Game event received:', event); - - switch (event.type) { - case 'colony_update': - updateColony(event.data.colony_id, event.data.updates); - break; - - case 'fleet_update': - updateFleet(event.data.fleet_id, event.data.updates); - break; - - case 'research_complete': - updateResearch(event.data.research_id, { - is_researching: false, - level: event.data.new_level - }); - toast.success(`Research completed: ${event.data.technology_name}`); - break; - - case 'building_complete': - updateColony(event.data.colony_id, { - buildings: event.data.buildings - }); - toast.success(`Building completed: ${event.data.building_name}`); - break; - - case 'resource_update': - updateColony(event.data.colony_id, { - resources: event.data.resources - }); - break; - - default: - console.log('Unhandled game event type:', event.type); - } - }; - - const sendMessage = (type: string, data: any) => { - if (socketRef.current?.connected) { - socketRef.current.emit(type, data); - } else { - console.warn('Cannot send message: WebSocket not connected'); - } - }; - - // Effect to handle connection lifecycle - useEffect(() => { - if (autoConnect && isAuthenticated && token) { - connect(); - } else if (!isAuthenticated) { - disconnect(); - } - - return () => { - disconnect(); - }; - }, [isAuthenticated, token, autoConnect]); - - // Cleanup on unmount - useEffect(() => { - return () => { - disconnect(); - }; - }, []); - - return { - isConnected, - isConnecting, - connect, - disconnect, - sendMessage, - }; -}; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css deleted file mode 100644 index 59532a7..0000000 --- a/frontend/src/index.css +++ /dev/null @@ -1,67 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -/* Custom scrollbar styles */ -@layer utilities { - .scrollbar-thin { - scrollbar-width: thin; - scrollbar-color: rgb(71 85 105) transparent; - } - - .scrollbar-thin::-webkit-scrollbar { - width: 6px; - } - - .scrollbar-thin::-webkit-scrollbar-track { - background: transparent; - } - - .scrollbar-thin::-webkit-scrollbar-thumb { - background-color: rgb(71 85 105); - border-radius: 3px; - } - - .scrollbar-thin::-webkit-scrollbar-thumb:hover { - background-color: rgb(100 116 139); - } -} - -/* Game-specific styles */ -@layer components { - .btn-primary { - @apply bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2; - } - - .btn-secondary { - @apply bg-dark-700 hover:bg-dark-600 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-dark-500 focus:ring-offset-2; - } - - .card { - @apply bg-dark-800 border border-dark-700 rounded-lg p-6 shadow-lg; - } - - .input-field { - @apply w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-dark-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500; - } - - .resource-display { - @apply flex items-center space-x-2 px-3 py-2 bg-dark-700 rounded-lg border border-dark-600; - } -} - -/* Base styles */ -body { - @apply bg-dark-900 text-white font-sans antialiased; - margin: 0; - min-height: 100vh; -} - -/* Loading animations */ -.loading-pulse { - @apply animate-pulse bg-dark-700 rounded; -} - -.loading-spinner { - @apply animate-spin rounded-full border-2 border-dark-600 border-t-primary-500; -} \ No newline at end of file diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts deleted file mode 100644 index db5b45e..0000000 --- a/frontend/src/lib/api.ts +++ /dev/null @@ -1,193 +0,0 @@ -import axios, { type AxiosResponse, AxiosError } from 'axios'; -import type { ApiResponse } from '../types'; - -// Create axios instance with base configuration -const api = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000', - timeout: 10000, - headers: { - 'Content-Type': 'application/json', - }, -}); - -// Request interceptor to add auth token -api.interceptors.request.use( - (config) => { - const token = localStorage.getItem('auth_token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => { - return Promise.reject(error); - } -); - -// Response interceptor for error handling -api.interceptors.response.use( - (response: AxiosResponse) => { - return response; - }, - (error: AxiosError) => { - // Handle token expiration - if (error.response?.status === 401) { - localStorage.removeItem('auth_token'); - localStorage.removeItem('user_data'); - window.location.href = '/login'; - } - - // Handle network errors - if (!error.response) { - console.error('Network error:', error.message); - } - - return Promise.reject(error); - } -); - -// API methods -export const apiClient = { - // Authentication - auth: { - login: (credentials: { email: string; password: string }) => - api.post>('/api/auth/login', credentials), - - register: (userData: { username: string; email: string; password: string }) => - api.post>('/api/auth/register', userData), - - logout: () => - api.post>('/api/auth/logout'), - - forgotPassword: (email: string) => - api.post>('/api/auth/forgot-password', { email }), - - resetPassword: (token: string, password: string) => - api.post>('/api/auth/reset-password', { token, password }), - - verifyEmail: (token: string) => - api.post>('/api/auth/verify-email', { token }), - - refreshToken: () => - api.post>('/api/auth/refresh'), - }, - - // Player - player: { - getProfile: () => - api.get>('/api/player/profile'), - - updateProfile: (profileData: any) => - api.put>('/api/player/profile', profileData), - - getStats: () => - api.get>('/api/player/stats'), - }, - - // Colonies - colonies: { - getAll: () => - api.get>('/api/player/colonies'), - - getById: (id: number) => - api.get>(`/api/player/colonies/${id}`), - - create: (colonyData: { name: string; coordinates: string; planet_type_id: number }) => - api.post>('/api/player/colonies', colonyData), - - update: (id: number, colonyData: any) => - api.put>(`/api/player/colonies/${id}`, colonyData), - - delete: (id: number) => - api.delete>(`/api/player/colonies/${id}`), - - getBuildings: (colonyId: number) => - api.get>(`/api/player/colonies/${colonyId}/buildings`), - - constructBuilding: (colonyId: number, buildingData: { building_type_id: number }) => - api.post>(`/api/player/colonies/${colonyId}/buildings`, buildingData), - - upgradeBuilding: (colonyId: number, buildingId: number) => - api.put>(`/api/player/colonies/${colonyId}/buildings/${buildingId}/upgrade`), - }, - - // Resources - resources: { - getByColony: (colonyId: number) => - api.get>(`/api/player/colonies/${colonyId}/resources`), - - getTotal: () => - api.get>('/api/player/resources'), - }, - - // Fleets - fleets: { - getAll: () => - api.get>('/api/player/fleets'), - - getById: (id: number) => - api.get>(`/api/player/fleets/${id}`), - - create: (fleetData: { name: string; colony_id: number; ships: any[] }) => - api.post>('/api/player/fleets', fleetData), - - update: (id: number, fleetData: any) => - api.put>(`/api/player/fleets/${id}`, fleetData), - - delete: (id: number) => - api.delete>(`/api/player/fleets/${id}`), - - move: (id: number, destination: string) => - api.post>(`/api/player/fleets/${id}/move`, { destination }), - }, - - // Research - research: { - getAll: () => - api.get>('/api/player/research'), - - getTechnologies: () => - api.get>('/api/player/research/technologies'), - - start: (technologyId: number) => - api.post>('/api/player/research/start', { technology_id: technologyId }), - - cancel: (researchId: number) => - api.post>(`/api/player/research/${researchId}/cancel`), - }, - - // Galaxy - galaxy: { - getSectors: () => - api.get>('/api/player/galaxy/sectors'), - - getSector: (coordinates: string) => - api.get>(`/api/player/galaxy/sectors/${coordinates}`), - - scan: (coordinates: string) => - api.post>('/api/player/galaxy/scan', { coordinates }), - }, - - // Events - events: { - getAll: (limit?: number) => - api.get>('/api/player/events', { params: { limit } }), - - markRead: (eventId: number) => - api.put>(`/api/player/events/${eventId}/read`), - }, - - // Notifications - notifications: { - getAll: () => - api.get>('/api/player/notifications'), - - markRead: (notificationId: number) => - api.put>(`/api/player/notifications/${notificationId}/read`), - - markAllRead: () => - api.put>('/api/player/notifications/read-all'), - }, -}; - -export default api; \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx deleted file mode 100644 index bef5202..0000000 --- a/frontend/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/frontend/src/pages/Colonies.tsx b/frontend/src/pages/Colonies.tsx deleted file mode 100644 index fdd96ab..0000000 --- a/frontend/src/pages/Colonies.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; -import { - BuildingOfficeIcon, - PlusIcon, - MapPinIcon, - UsersIcon, - HeartIcon, -} from '@heroicons/react/24/outline'; -import { useGameStore } from '../store/gameStore'; - -const Colonies: React.FC = () => { - const { - colonies, - loading, - fetchColonies, - selectColony, - } = useGameStore(); - - const [sortBy, setSortBy] = useState<'name' | 'population' | 'founded_at'>('name'); - const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); - - useEffect(() => { - fetchColonies(); - }, [fetchColonies]); - - const sortedColonies = [...colonies].sort((a, b) => { - let aValue: string | number; - let bValue: string | number; - - switch (sortBy) { - case 'name': - aValue = a.name.toLowerCase(); - bValue = b.name.toLowerCase(); - break; - case 'population': - aValue = a.population; - bValue = b.population; - break; - case 'founded_at': - aValue = new Date(a.founded_at).getTime(); - bValue = new Date(b.founded_at).getTime(); - break; - default: - aValue = a.name.toLowerCase(); - bValue = b.name.toLowerCase(); - } - - if (sortOrder === 'asc') { - return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; - } else { - return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; - } - }); - - const handleSort = (field: typeof sortBy) => { - if (sortBy === field) { - setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); - } else { - setSortBy(field); - setSortOrder('asc'); - } - }; - - const getMoraleColor = (morale: number) => { - if (morale >= 80) return 'text-green-400'; - if (morale >= 60) return 'text-yellow-400'; - if (morale >= 40) return 'text-orange-400'; - return 'text-red-400'; - }; - - const getMoraleIcon = (morale: number) => { - if (morale >= 80) return '😊'; - if (morale >= 60) return '😐'; - if (morale >= 40) return '😟'; - return '😰'; - }; - - if (loading.colonies) { - return ( -
-
-

Colonies

-
-
- {[...Array(6)].map((_, i) => ( -
-
-
- ))} -
-
- ); - } - - return ( -
- {/* Header */} -
-
-

Colonies

-

- Manage your {colonies.length} colonies across the galaxy -

-
- - - Found Colony - -
- - {/* Sort Controls */} -
-
- Sort by: - - - -
-
- - {/* Colonies Grid */} - {sortedColonies.length > 0 ? ( -
- {sortedColonies.map((colony) => ( - selectColony(colony)} - className="card hover:bg-dark-700 transition-colors duration-200 cursor-pointer" - > -
- {/* Colony Header */} -
-
-

{colony.name}

-
- - {colony.coordinates} -
-
- -
- - {/* Colony Stats */} -
-
- -
-

Population

-

- {colony.population.toLocaleString()} -

-
-
- -
- -
-

Morale

-

- {colony.morale}% {getMoraleIcon(colony.morale)} -

-
-
-
- - {/* Planet Type */} - {colony.planet_type && ( -
-

Planet Type

-

{colony.planet_type.name}

-
- )} - - {/* Resources Preview */} - {colony.resources && ( -
-

Resources

-
-
- Scrap: - - {colony.resources.scrap.toLocaleString()} - -
-
- Energy: - - {colony.resources.energy.toLocaleString()} - -
-
-
- )} - - {/* Founded Date */} -
-

- Founded {new Date(colony.founded_at).toLocaleDateString()} -

-
-
- - ))} -
- ) : ( -
- -

No Colonies Yet

-

- Start your galactic empire by founding your first colony -

- - - Found Your First Colony - -
- )} -
- ); -}; - -export default Colonies; \ No newline at end of file diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx deleted file mode 100644 index 7bd875d..0000000 --- a/frontend/src/pages/Dashboard.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import React, { useEffect } from 'react'; -import { Link } from 'react-router-dom'; -import { - BuildingOfficeIcon, - RocketLaunchIcon, - BeakerIcon, - PlusIcon, -} from '@heroicons/react/24/outline'; -import { useAuthStore } from '../store/authStore'; -import { useGameStore } from '../store/gameStore'; - -const Dashboard: React.FC = () => { - const { user } = useAuthStore(); - const { - colonies, - fleets, - research, - totalResources, - loading, - fetchColonies, - fetchFleets, - fetchResearch, - fetchTotalResources, - } = useGameStore(); - - useEffect(() => { - // Fetch initial data when component mounts - fetchColonies(); - fetchFleets(); - fetchResearch(); - fetchTotalResources(); - }, [fetchColonies, fetchFleets, fetchResearch, fetchTotalResources]); - - const stats = [ - { - name: 'Colonies', - value: colonies.length, - icon: BuildingOfficeIcon, - href: '/colonies', - color: 'text-green-400', - loading: loading.colonies, - }, - { - name: 'Fleets', - value: fleets.length, - icon: RocketLaunchIcon, - href: '/fleets', - color: 'text-blue-400', - loading: loading.fleets, - }, - { - name: 'Research Projects', - value: research.filter(r => r.is_researching).length, - icon: BeakerIcon, - href: '/research', - color: 'text-purple-400', - loading: loading.research, - }, - ]; - - const recentColonies = colonies.slice(0, 3); - const activeResearch = research.filter(r => r.is_researching).slice(0, 3); - - return ( -
- {/* Welcome Header */} -
-

- Welcome back, {user?.username}! -

-

- Command your forces across the shattered galaxy. Your empire awaits your orders. -

-
- - {/* Quick Stats */} -
- {stats.map((stat) => { - const Icon = stat.icon; - return ( - -
-
- -
-
-

{stat.name}

-

- {stat.loading ? ( -

- ) : ( - stat.value - )} -

-
-
- - ); - })} -
- - {/* Resources Overview */} - {totalResources && ( -
-

Resource Overview

-
-
- Scrap - - {totalResources.scrap.toLocaleString()} - -
-
- Energy - - {totalResources.energy.toLocaleString()} - -
-
- Research - - {totalResources.research_points.toLocaleString()} - -
-
- Biomass - - {totalResources.biomass.toLocaleString()} - -
-
-
- )} - -
- {/* Recent Colonies */} -
-
-

Recent Colonies

- - View all - -
- - {loading.colonies ? ( -
- {[...Array(3)].map((_, i) => ( -
- ))} -
- ) : recentColonies.length > 0 ? ( -
- {recentColonies.map((colony) => ( - -
-
-

{colony.name}

-

{colony.coordinates}

-
-
-

Population

-

- {colony.population.toLocaleString()} -

-
-
- - ))} -
- ) : ( -
- -

No colonies yet

- - - Found your first colony - -
- )} -
- - {/* Active Research */} -
-
-

Active Research

- - View all - -
- - {loading.research ? ( -
- {[...Array(3)].map((_, i) => ( -
- ))} -
- ) : activeResearch.length > 0 ? ( -
- {activeResearch.map((research) => ( -
-
-
-

- {research.technology?.name} -

-

- Level {research.level} -

-
-
-
-
-
-

In progress

-
-
-
- ))} -
- ) : ( -
- -

No active research

- - - Start research - -
- )} -
-
-
- ); -}; - -export default Dashboard; \ No newline at end of file diff --git a/frontend/src/store/authStore.ts b/frontend/src/store/authStore.ts deleted file mode 100644 index 7990e44..0000000 --- a/frontend/src/store/authStore.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import type { AuthState, LoginCredentials, RegisterCredentials } from '../types'; -import { apiClient } from '../lib/api'; -import toast from 'react-hot-toast'; - -interface AuthStore extends AuthState { - // Actions - login: (credentials: LoginCredentials) => Promise; - register: (credentials: RegisterCredentials) => Promise; - logout: () => void; - refreshUser: () => Promise; - clearError: () => void; - setLoading: (loading: boolean) => void; -} - -export const useAuthStore = create()( - persist( - (set) => ({ - // Initial state - user: null, - token: null, - isAuthenticated: false, - isLoading: false, - - // Login action - login: async (credentials: LoginCredentials) => { - set({ isLoading: true }); - - try { - const response = await apiClient.auth.login(credentials); - - if (response.data.success && response.data.data) { - const { user, token } = response.data.data; - - // Store token in localStorage for API client - localStorage.setItem('auth_token', token); - - set({ - user, - token, - isAuthenticated: true, - isLoading: false, - }); - - toast.success(`Welcome back, ${user.username}!`); - return true; - } else { - toast.error(response.data.error || 'Login failed'); - set({ isLoading: false }); - return false; - } - } catch (error: any) { - const message = error.response?.data?.error || 'Login failed'; - toast.error(message); - set({ isLoading: false }); - return false; - } - }, - - // Register action - register: async (credentials: RegisterCredentials) => { - set({ isLoading: true }); - - try { - const { confirmPassword, ...registerData } = credentials; - - // Validate passwords match - if (credentials.password !== confirmPassword) { - toast.error('Passwords do not match'); - set({ isLoading: false }); - return false; - } - - const response = await apiClient.auth.register(registerData); - - if (response.data.success && response.data.data) { - const { user, token } = response.data.data; - - // Store token in localStorage for API client - localStorage.setItem('auth_token', token); - - set({ - user, - token, - isAuthenticated: true, - isLoading: false, - }); - - toast.success(`Welcome to Shattered Void, ${user.username}!`); - return true; - } else { - toast.error(response.data.error || 'Registration failed'); - set({ isLoading: false }); - return false; - } - } catch (error: any) { - const message = error.response?.data?.error || 'Registration failed'; - toast.error(message); - set({ isLoading: false }); - return false; - } - }, - - // Logout action - logout: () => { - try { - // Call logout endpoint to invalidate token on server - apiClient.auth.logout().catch(() => { - // Ignore errors on logout endpoint - }); - } catch (error) { - // Ignore errors - } - - // Clear local storage - localStorage.removeItem('auth_token'); - localStorage.removeItem('user_data'); - - // Clear store state - set({ - user: null, - token: null, - isAuthenticated: false, - isLoading: false, - }); - - toast.success('Logged out successfully'); - }, - - // Refresh user data - refreshUser: async () => { - const token = localStorage.getItem('auth_token'); - if (!token) return; - - try { - const response = await apiClient.player.getProfile(); - - if (response.data.success && response.data.data) { - set({ user: response.data.data }); - } - } catch (error) { - // If refresh fails, user might need to re-login - console.error('Failed to refresh user data:', error); - } - }, - - // Clear error state - clearError: () => { - // This can be extended if we add error state - }, - - // Set loading state - setLoading: (loading: boolean) => { - set({ isLoading: loading }); - }, - }), - { - name: 'auth-storage', - partialize: (state) => ({ - user: state.user, - token: state.token, - isAuthenticated: state.isAuthenticated, - }), - } - ) -); \ No newline at end of file diff --git a/frontend/src/store/gameStore.ts b/frontend/src/store/gameStore.ts deleted file mode 100644 index 92e1d71..0000000 --- a/frontend/src/store/gameStore.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { create } from 'zustand'; -import type { Colony, Fleet, Resources, Research } from '../types'; -import { apiClient } from '../lib/api'; -import toast from 'react-hot-toast'; - -interface GameState { - // Data - colonies: Colony[]; - fleets: Fleet[]; - totalResources: Resources | null; - research: Research[]; - - // Loading states - loading: { - colonies: boolean; - fleets: boolean; - resources: boolean; - research: boolean; - }; - - // Selected entities - selectedColony: Colony | null; - selectedFleet: Fleet | null; -} - -interface GameStore extends GameState { - // Colony actions - fetchColonies: () => Promise; - selectColony: (colony: Colony | null) => void; - createColony: (colonyData: { name: string; coordinates: string; planet_type_id: number }) => Promise; - updateColony: (colonyId: number, updates: Partial) => void; - - // Fleet actions - fetchFleets: () => Promise; - selectFleet: (fleet: Fleet | null) => void; - createFleet: (fleetData: { name: string; colony_id: number; ships: any[] }) => Promise; - updateFleet: (fleetId: number, updates: Partial) => void; - - // Resource actions - fetchTotalResources: () => Promise; - updateColonyResources: (colonyId: number, resources: Resources) => void; - - // Research actions - fetchResearch: () => Promise; - startResearch: (technologyId: number) => Promise; - updateResearch: (researchId: number, updates: Partial) => void; - - // Utility actions - setLoading: (key: keyof GameState['loading'], loading: boolean) => void; - clearData: () => void; -} - -export const useGameStore = create((set, get) => ({ - // Initial state - colonies: [], - fleets: [], - totalResources: null, - research: [], - loading: { - colonies: false, - fleets: false, - resources: false, - research: false, - }, - selectedColony: null, - selectedFleet: null, - - // Colony actions - fetchColonies: async () => { - set(state => ({ loading: { ...state.loading, colonies: true } })); - - try { - const response = await apiClient.colonies.getAll(); - - if (response.data.success && response.data.data) { - set({ - colonies: response.data.data, - loading: { ...get().loading, colonies: false } - }); - } - } catch (error: any) { - console.error('Failed to fetch colonies:', error); - toast.error('Failed to load colonies'); - set(state => ({ loading: { ...state.loading, colonies: false } })); - } - }, - - selectColony: (colony: Colony | null) => { - set({ selectedColony: colony }); - }, - - createColony: async (colonyData) => { - try { - const response = await apiClient.colonies.create(colonyData); - - if (response.data.success && response.data.data) { - const newColony = response.data.data; - set(state => ({ - colonies: [...state.colonies, newColony] - })); - toast.success(`Colony "${colonyData.name}" founded successfully!`); - return true; - } else { - toast.error(response.data.error || 'Failed to create colony'); - return false; - } - } catch (error: any) { - const message = error.response?.data?.error || 'Failed to create colony'; - toast.error(message); - return false; - } - }, - - updateColony: (colonyId: number, updates: Partial) => { - set(state => ({ - colonies: state.colonies.map(colony => - colony.id === colonyId ? { ...colony, ...updates } : colony - ), - selectedColony: state.selectedColony?.id === colonyId - ? { ...state.selectedColony, ...updates } - : state.selectedColony - })); - }, - - // Fleet actions - fetchFleets: async () => { - set(state => ({ loading: { ...state.loading, fleets: true } })); - - try { - const response = await apiClient.fleets.getAll(); - - if (response.data.success && response.data.data) { - set({ - fleets: response.data.data, - loading: { ...get().loading, fleets: false } - }); - } - } catch (error: any) { - console.error('Failed to fetch fleets:', error); - toast.error('Failed to load fleets'); - set(state => ({ loading: { ...state.loading, fleets: false } })); - } - }, - - selectFleet: (fleet: Fleet | null) => { - set({ selectedFleet: fleet }); - }, - - createFleet: async (fleetData) => { - try { - const response = await apiClient.fleets.create(fleetData); - - if (response.data.success && response.data.data) { - const newFleet = response.data.data; - set(state => ({ - fleets: [...state.fleets, newFleet] - })); - toast.success(`Fleet "${fleetData.name}" created successfully!`); - return true; - } else { - toast.error(response.data.error || 'Failed to create fleet'); - return false; - } - } catch (error: any) { - const message = error.response?.data?.error || 'Failed to create fleet'; - toast.error(message); - return false; - } - }, - - updateFleet: (fleetId: number, updates: Partial) => { - set(state => ({ - fleets: state.fleets.map(fleet => - fleet.id === fleetId ? { ...fleet, ...updates } : fleet - ), - selectedFleet: state.selectedFleet?.id === fleetId - ? { ...state.selectedFleet, ...updates } - : state.selectedFleet - })); - }, - - // Resource actions - fetchTotalResources: async () => { - set(state => ({ loading: { ...state.loading, resources: true } })); - - try { - const response = await apiClient.resources.getTotal(); - - if (response.data.success && response.data.data) { - set({ - totalResources: response.data.data, - loading: { ...get().loading, resources: false } - }); - } - } catch (error: any) { - console.error('Failed to fetch resources:', error); - set(state => ({ loading: { ...state.loading, resources: false } })); - } - }, - - updateColonyResources: (colonyId: number, resources: Resources) => { - set(state => ({ - colonies: state.colonies.map(colony => - colony.id === colonyId - ? { - ...colony, - resources: colony.resources - ? { ...colony.resources, ...resources } - : undefined - } - : colony - ) - })); - }, - - // Research actions - fetchResearch: async () => { - set(state => ({ loading: { ...state.loading, research: true } })); - - try { - const response = await apiClient.research.getAll(); - - if (response.data.success && response.data.data) { - set({ - research: response.data.data, - loading: { ...get().loading, research: false } - }); - } - } catch (error: any) { - console.error('Failed to fetch research:', error); - toast.error('Failed to load research'); - set(state => ({ loading: { ...state.loading, research: false } })); - } - }, - - startResearch: async (technologyId: number) => { - try { - const response = await apiClient.research.start(technologyId); - - if (response.data.success && response.data.data) { - const newResearch = response.data.data; - set(state => ({ - research: [...state.research, newResearch] - })); - toast.success('Research started successfully!'); - return true; - } else { - toast.error(response.data.error || 'Failed to start research'); - return false; - } - } catch (error: any) { - const message = error.response?.data?.error || 'Failed to start research'; - toast.error(message); - return false; - } - }, - - updateResearch: (researchId: number, updates: Partial) => { - set(state => ({ - research: state.research.map(research => - research.id === researchId ? { ...research, ...updates } : research - ) - })); - }, - - // Utility actions - setLoading: (key: keyof GameState['loading'], loading: boolean) => { - set(state => ({ - loading: { ...state.loading, [key]: loading } - })); - }, - - clearData: () => { - set({ - colonies: [], - fleets: [], - totalResources: null, - research: [], - selectedColony: null, - selectedFleet: null, - loading: { - colonies: false, - fleets: false, - resources: false, - research: false, - } - }); - }, -})); \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts deleted file mode 100644 index 0f1e857..0000000 --- a/frontend/src/types/index.ts +++ /dev/null @@ -1,200 +0,0 @@ -// Authentication types -export interface User { - id: number; - username: string; - email: string; - created_at: string; - last_login?: string; -} - -export interface AuthState { - user: User | null; - token: string | null; - isAuthenticated: boolean; - isLoading: boolean; -} - -export interface LoginCredentials { - email: string; - password: string; -} - -export interface RegisterCredentials { - username: string; - email: string; - password: string; - confirmPassword: string; -} - -// Colony types -export interface Colony { - id: number; - player_id: number; - name: string; - coordinates: string; - planet_type_id: number; - population: number; - morale: number; - founded_at: string; - last_updated: string; - planet_type?: PlanetType; - buildings?: Building[]; - resources?: ColonyResources; -} - -export interface PlanetType { - id: number; - name: string; - description: string; - resource_modifiers: Record; -} - -export interface Building { - id: number; - colony_id: number; - building_type_id: number; - level: number; - construction_start?: string; - construction_end?: string; - is_constructing: boolean; - building_type?: BuildingType; -} - -export interface BuildingType { - id: number; - name: string; - description: string; - category: string; - base_cost: Record; - base_production: Record; - max_level: number; -} - -// Resource types -export interface Resources { - scrap: number; - energy: number; - research_points: number; - biomass: number; -} - -export interface ColonyResources extends Resources { - colony_id: number; - last_updated: string; - production_rates: Resources; -} - -// Fleet types -export interface Fleet { - id: number; - player_id: number; - name: string; - location_type: 'colony' | 'space'; - location_id?: number; - coordinates?: string; - status: 'docked' | 'moving' | 'in_combat'; - destination?: string; - arrival_time?: string; - ships: FleetShip[]; -} - -export interface FleetShip { - id: number; - fleet_id: number; - design_id: number; - quantity: number; - ship_design?: ShipDesign; -} - -export interface ShipDesign { - id: number; - name: string; - hull_type: string; - cost: Record; - stats: { - attack: number; - defense: number; - health: number; - speed: number; - cargo: number; - }; -} - -// Research types -export interface Research { - id: number; - player_id: number; - technology_id: number; - level: number; - research_start?: string; - research_end?: string; - is_researching: boolean; - technology?: Technology; -} - -export interface Technology { - id: number; - name: string; - description: string; - category: string; - base_cost: number; - max_level: number; - prerequisites: number[]; - unlocks: string[]; -} - -// WebSocket types -export interface WebSocketMessage { - type: string; - data: any; - timestamp: string; -} - -export interface GameEvent { - id: string; - type: 'colony_update' | 'resource_update' | 'fleet_update' | 'research_complete' | 'building_complete'; - data: any; - timestamp: string; -} - -// API Response types -export interface ApiResponse { - success: boolean; - data?: T; - error?: string; - message?: string; -} - -export interface PaginatedResponse { - data: T[]; - total: number; - page: number; - limit: number; - totalPages: number; -} - -// UI State types -export interface LoadingState { - [key: string]: boolean; -} - -export interface ErrorState { - [key: string]: string | null; -} - -// Navigation types -export interface NavItem { - name: string; - href: string; - icon?: React.ComponentType; - current?: boolean; - badge?: number; -} - -// Toast notification types -export interface ToastOptions { - type: 'success' | 'error' | 'warning' | 'info'; - title: string; - message?: string; - duration?: number; -} \ No newline at end of file diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/frontend/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js deleted file mode 100644 index 0d00cf6..0000000 --- a/frontend/tailwind.config.js +++ /dev/null @@ -1,56 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: { - colors: { - primary: { - 50: '#eff6ff', - 100: '#dbeafe', - 200: '#bfdbfe', - 300: '#93c5fd', - 400: '#60a5fa', - 500: '#3b82f6', - 600: '#2563eb', - 700: '#1d4ed8', - 800: '#1e40af', - 900: '#1e3a8a', - }, - dark: { - 50: '#f8fafc', - 100: '#f1f5f9', - 200: '#e2e8f0', - 300: '#cbd5e1', - 400: '#94a3b8', - 500: '#64748b', - 600: '#475569', - 700: '#334155', - 800: '#1e293b', - 900: '#0f172a', - } - }, - fontFamily: { - 'mono': ['JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', 'monospace'], - }, - animation: { - 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', - 'fade-in': 'fadeIn 0.5s ease-out', - 'slide-in': 'slideIn 0.3s ease-out', - }, - keyframes: { - fadeIn: { - '0%': { opacity: '0' }, - '100%': { opacity: '1' }, - }, - slideIn: { - '0%': { transform: 'translateY(-10px)', opacity: '0' }, - '100%': { transform: 'translateY(0)', opacity: '1' }, - } - } - }, - }, - plugins: [], -} \ No newline at end of file diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json deleted file mode 100644 index 227a6c6..0000000 --- a/frontend/tsconfig.app.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["src"] -} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index 1ffef60..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json deleted file mode 100644 index f85a399..0000000 --- a/frontend/tsconfig.node.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2023", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index 3c72d2f..0000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import path from 'path' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], - server: { - port: 5173, - host: true, - proxy: { - '/api': { - target: 'http://localhost:3000', - changeOrigin: true, - secure: false, - }, - '/socket.io': { - target: 'http://localhost:3000', - changeOrigin: true, - ws: true, - } - } - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - build: { - outDir: 'dist', - sourcemap: true, - rollupOptions: { - output: { - manualChunks: { - vendor: ['react', 'react-dom'], - router: ['react-router-dom'], - ui: ['@headlessui/react', '@heroicons/react'], - }, - }, - }, - }, - optimizeDeps: { - include: ['react', 'react-dom', 'react-router-dom'], - }, -}) diff --git a/package.json b/package.json index f03525f..012b56d 100644 --- a/package.json +++ b/package.json @@ -6,37 +6,24 @@ "scripts": { "dev": "nodemon --inspect=0.0.0.0:9229 src/server.js", "start": "node src/server.js", - "start:game": "node start-game.js", - "start:dev": "NODE_ENV=development node start-game.js", - "start:prod": "NODE_ENV=production node start-game.js", - "start:staging": "NODE_ENV=staging node start-game.js", - "start:quick": "./start.sh", - "start:debug": "./start.sh --debug --verbose", - "start:no-frontend": "./start.sh --no-frontend", - "start:backend-only": "ENABLE_FRONTEND=false node start-game.js", - "game": "./start.sh", "test": "jest --verbose --coverage", "test:watch": "jest --watch --verbose", "test:integration": "jest --testPathPattern=integration --runInBand", "test:e2e": "jest --testPathPattern=e2e --runInBand", "lint": "eslint src/ --ext .js --fix", "lint:check": "eslint src/ --ext .js", - "health:check": "node -e \"require('./scripts/health-monitor'); console.log('Health monitoring available')\"", - "system:check": "node -e \"const checks = require('./scripts/startup-checks'); new checks().runAllChecks().then(r => console.log('System checks:', r.success ? 'PASSED' : 'FAILED'))\"", "db:migrate": "knex migrate:latest", "db:rollback": "knex migrate:rollback", "db:seed": "knex seed:run", "db:reset": "knex migrate:rollback:all && knex migrate:latest && knex seed:run", "db:setup": "createdb shattered_void_dev && npm run db:migrate && npm run db:seed", - "db:validate": "node -e \"const val = require('./scripts/database-validator'); new val().validateDatabase().then(r => console.log('DB validation:', r.success ? 'PASSED' : 'FAILED', r.error || ''))\"", "setup": "node scripts/setup.js", "docker:build": "docker build -t shattered-void .", "docker:run": "docker-compose up -d", "docker:dev": "docker-compose -f docker-compose.dev.yml up -d", "logs": "tail -f logs/combined.log", "logs:error": "tail -f logs/error.log", - "logs:audit": "tail -f logs/audit.log", - "logs:startup": "tail -f logs/startup.log" + "logs:audit": "tail -f logs/audit.log" }, "dependencies": { "bcrypt": "^5.1.1", diff --git a/scripts/database-validator.js b/scripts/database-validator.js deleted file mode 100644 index 50bd138..0000000 --- a/scripts/database-validator.js +++ /dev/null @@ -1,622 +0,0 @@ -/** - * Shattered Void MMO - Database Validation System - * - * This module provides comprehensive database validation including connectivity, - * schema validation, migration status, and data integrity checks. - */ - -const path = require('path'); -const fs = require('fs').promises; - -class DatabaseValidator { - constructor() { - this.knex = null; - this.validationResults = { - connectivity: false, - migrations: false, - schema: false, - seeds: false, - integrity: false - }; - } - - /** - * Validate complete database setup - */ - async validateDatabase() { - const startTime = Date.now(); - const results = { - success: false, - connectivity: null, - migrations: null, - schema: null, - seeds: null, - integrity: null, - error: null, - duration: 0 - }; - - try { - // Test database connectivity - results.connectivity = await this.validateConnectivity(); - - // Check migration status - results.migrations = await this.validateMigrations(); - - // Validate schema structure - results.schema = await this.validateSchema(); - - // Check seed data - results.seeds = await this.validateSeeds(); - - // Run integrity checks - results.integrity = await this.validateIntegrity(); - - // Determine overall success - results.success = results.connectivity.success && - results.migrations.success && - results.schema.success; - - results.duration = Date.now() - startTime; - return results; - - } catch (error) { - results.error = error.message; - results.duration = Date.now() - startTime; - return results; - } finally { - // Cleanup database connection - if (this.knex) { - await this.knex.destroy(); - } - } - } - - /** - * Validate database connectivity - */ - async validateConnectivity() { - try { - // Load database configuration - const knexConfig = this.loadKnexConfig(); - const config = knexConfig[process.env.NODE_ENV || 'development']; - - if (!config) { - throw new Error(`No database configuration found for environment: ${process.env.NODE_ENV || 'development'}`); - } - - // Initialize Knex connection - this.knex = require('knex')(config); - - // Test basic connectivity - await this.knex.raw('SELECT 1 as test'); - - // Get database version info - const versionResult = await this.knex.raw('SELECT version()'); - const version = versionResult.rows[0].version; - - // Get database size info - const sizeResult = await this.knex.raw(` - SELECT pg_database.datname, - pg_size_pretty(pg_database_size(pg_database.datname)) AS size - FROM pg_database - WHERE pg_database.datname = current_database() - `); - - const dbSize = sizeResult.rows[0]?.size || 'Unknown'; - - // Check connection pool status - const poolInfo = { - min: this.knex.client.pool.min, - max: this.knex.client.pool.max, - used: this.knex.client.pool.numUsed(), - free: this.knex.client.pool.numFree(), - pending: this.knex.client.pool.numPendingAcquires() - }; - - return { - success: true, - database: config.connection.database, - host: config.connection.host, - port: config.connection.port, - version: version.split(' ')[0] + ' ' + version.split(' ')[1], // PostgreSQL version - size: dbSize, - pool: poolInfo, - ssl: config.connection.ssl ? 'enabled' : 'disabled' - }; - - } catch (error) { - return { - success: false, - error: error.message, - troubleshooting: this.getDatabaseTroubleshooting(error) - }; - } - } - - /** - * Validate migration status - */ - async validateMigrations() { - try { - // Check if migrations table exists - const hasTable = await this.knex.schema.hasTable('knex_migrations'); - - if (!hasTable) { - // Run migrations if table doesn't exist - console.log(' 📦 Running initial database migrations...'); - await this.knex.migrate.latest(); - } - - // Get migration status - const [currentBatch, migrationList] = await Promise.all([ - this.knex.migrate.currentVersion(), - this.knex.migrate.list() - ]); - - const [completed, pending] = migrationList; - - // Check for pending migrations - if (pending.length > 0) { - console.log(` 📦 Found ${pending.length} pending migrations, running now...`); - await this.knex.migrate.latest(); - - // Re-check status after running migrations - const [newCompleted] = await this.knex.migrate.list(); - - return { - success: true, - currentBatch: await this.knex.migrate.currentVersion(), - completed: newCompleted.length, - pending: 0, - autoRan: pending.length, - migrations: newCompleted.map(migration => ({ - name: migration, - status: 'completed' - })) - }; - } - - return { - success: true, - currentBatch, - completed: completed.length, - pending: pending.length, - migrations: [ - ...completed.map(migration => ({ - name: migration, - status: 'completed' - })), - ...pending.map(migration => ({ - name: migration, - status: 'pending' - })) - ] - }; - - } catch (error) { - return { - success: false, - error: error.message, - troubleshooting: [ - 'Check if migration files exist in src/database/migrations/', - 'Verify database user has CREATE permissions', - 'Ensure migration files follow correct naming convention' - ] - }; - } - } - - /** - * Validate database schema structure - */ - async validateSchema() { - try { - const requiredTables = [ - 'players', - 'colonies', - 'player_resources', - 'fleets', - 'fleet_ships', - 'ship_designs', - 'technologies', - 'player_research' - ]; - - const schemaInfo = { - tables: {}, - missingTables: [], - totalTables: 0, - requiredTables: requiredTables.length - }; - - // Check each required table - for (const tableName of requiredTables) { - const exists = await this.knex.schema.hasTable(tableName); - - if (exists) { - // Get table info - const columns = await this.knex(tableName).columnInfo(); - const rowCount = await this.knex(tableName).count('* as count').first(); - - schemaInfo.tables[tableName] = { - exists: true, - columns: Object.keys(columns).length, - rows: parseInt(rowCount.count), - structure: Object.keys(columns) - }; - } else { - schemaInfo.missingTables.push(tableName); - schemaInfo.tables[tableName] = { - exists: false, - error: 'Table does not exist' - }; - } - } - - // Get total number of tables in database - const allTables = await this.knex.raw(` - SELECT table_name - FROM information_schema.tables - WHERE table_schema = 'public' - AND table_type = 'BASE TABLE' - `); - - schemaInfo.totalTables = allTables.rows.length; - - const success = schemaInfo.missingTables.length === 0; - - return { - success, - ...schemaInfo, - coverage: `${requiredTables.length - schemaInfo.missingTables.length}/${requiredTables.length}`, - troubleshooting: !success ? [ - 'Run database migrations: npm run db:migrate', - 'Check migration files in src/database/migrations/', - 'Verify database user has CREATE permissions' - ] : null - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Validate seed data - */ - async validateSeeds() { - try { - const seedChecks = { - technologies: await this.checkTechnologiesSeeded(), - shipDesigns: await this.checkShipDesignsSeeded(), - systemData: await this.checkSystemDataSeeded() - }; - - const allSeeded = Object.values(seedChecks).every(check => check.seeded); - - // If no seed data, offer to run seeds - if (!allSeeded) { - console.log(' 🌱 Some seed data is missing, running seeds...'); - - try { - // Run seeds - await this.knex.seed.run(); - - // Re-check seed status - const newSeedChecks = { - technologies: await this.checkTechnologiesSeeded(), - shipDesigns: await this.checkShipDesignsSeeded(), - systemData: await this.checkSystemDataSeeded() - }; - - return { - success: true, - autoSeeded: true, - checks: newSeedChecks, - message: 'Seed data was missing and has been automatically populated' - }; - - } catch (seedError) { - return { - success: false, - autoSeeded: false, - error: `Failed to run seeds: ${seedError.message}`, - checks: seedChecks - }; - } - } - - return { - success: true, - checks: seedChecks, - message: 'All required seed data is present' - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Validate data integrity - */ - async validateIntegrity() { - try { - const integrityChecks = []; - - // Check foreign key constraints - integrityChecks.push(await this.checkForeignKeyIntegrity()); - - // Check for orphaned records - integrityChecks.push(await this.checkOrphanedRecords()); - - // Check data consistency - integrityChecks.push(await this.checkDataConsistency()); - - const allPassed = integrityChecks.every(check => check.passed); - - return { - success: allPassed, - checks: integrityChecks, - summary: `${integrityChecks.filter(c => c.passed).length}/${integrityChecks.length} integrity checks passed` - }; - - } catch (error) { - return { - success: false, - error: error.message - }; - } - } - - /** - * Check if technologies are seeded - */ - async checkTechnologiesSeeded() { - try { - const count = await this.knex('technologies').count('* as count').first(); - const techCount = parseInt(count.count); - - return { - seeded: techCount > 0, - count: techCount, - expected: '> 0' - }; - } catch (error) { - return { - seeded: false, - error: error.message - }; - } - } - - /** - * Check if ship designs are seeded - */ - async checkShipDesignsSeeded() { - try { - const count = await this.knex('ship_designs').count('* as count').first(); - const designCount = parseInt(count.count); - - return { - seeded: designCount > 0, - count: designCount, - expected: '> 0' - }; - } catch (error) { - return { - seeded: false, - error: error.message - }; - } - } - - /** - * Check if system data is seeded - */ - async checkSystemDataSeeded() { - try { - // Check if we have any basic game configuration - const hasBasicData = true; // For now, assume system data is OK if DB is accessible - - return { - seeded: hasBasicData, - message: 'System data validation passed' - }; - } catch (error) { - return { - seeded: false, - error: error.message - }; - } - } - - /** - * Check foreign key integrity - */ - async checkForeignKeyIntegrity() { - try { - // Check for any foreign key constraint violations - const violations = []; - - // Check colonies -> players - const orphanedColonies = await this.knex.raw(` - SELECT c.id, c.name FROM colonies c - LEFT JOIN players p ON c.player_id = p.id - WHERE p.id IS NULL - `); - - if (orphanedColonies.rows.length > 0) { - violations.push(`${orphanedColonies.rows.length} colonies without valid players`); - } - - // Check fleets -> players - const orphanedFleets = await this.knex.raw(` - SELECT f.id, f.name FROM fleets f - LEFT JOIN players p ON f.player_id = p.id - WHERE p.id IS NULL - `); - - if (orphanedFleets.rows.length > 0) { - violations.push(`${orphanedFleets.rows.length} fleets without valid players`); - } - - return { - passed: violations.length === 0, - name: 'Foreign Key Integrity', - violations: violations, - message: violations.length === 0 ? 'All foreign key constraints are valid' : `Found ${violations.length} violations` - }; - - } catch (error) { - return { - passed: false, - name: 'Foreign Key Integrity', - error: error.message - }; - } - } - - /** - * Check for orphaned records - */ - async checkOrphanedRecords() { - try { - const orphanedRecords = []; - - // This is a simplified check - in a real scenario you'd check all relationships - return { - passed: orphanedRecords.length === 0, - name: 'Orphaned Records Check', - orphaned: orphanedRecords, - message: 'No orphaned records found' - }; - - } catch (error) { - return { - passed: false, - name: 'Orphaned Records Check', - error: error.message - }; - } - } - - /** - * Check data consistency - */ - async checkDataConsistency() { - try { - const inconsistencies = []; - - // Example: Check if all players have at least one colony (if required by game rules) - // This would depend on your specific game rules - - return { - passed: inconsistencies.length === 0, - name: 'Data Consistency Check', - inconsistencies: inconsistencies, - message: 'Data consistency checks passed' - }; - - } catch (error) { - return { - passed: false, - name: 'Data Consistency Check', - error: error.message - }; - } - } - - /** - * Load Knex configuration - */ - loadKnexConfig() { - try { - const knexfilePath = path.join(process.cwd(), 'knexfile.js'); - delete require.cache[require.resolve(knexfilePath)]; - return require(knexfilePath); - } catch (error) { - throw new Error(`Cannot load knexfile.js: ${error.message}`); - } - } - - /** - * Get database troubleshooting tips - */ - getDatabaseTroubleshooting(error) { - const tips = []; - - if (error.message.includes('ECONNREFUSED')) { - tips.push('Database server is not running - start PostgreSQL service'); - tips.push('Check if database is running on correct host/port'); - } - - if (error.message.includes('authentication failed')) { - tips.push('Check database username and password in .env file'); - tips.push('Verify database user exists and has correct permissions'); - } - - if (error.message.includes('database') && error.message.includes('does not exist')) { - tips.push('Create database: createdb shattered_void_dev'); - tips.push('Or run: npm run db:setup'); - } - - if (error.message.includes('permission denied')) { - tips.push('Database user needs CREATE and ALTER permissions'); - tips.push('Check PostgreSQL user privileges'); - } - - if (tips.length === 0) { - tips.push('Check database connection parameters in .env file'); - tips.push('Ensure PostgreSQL is installed and running'); - tips.push('Verify network connectivity to database server'); - } - - return tips; - } - - /** - * Get database performance metrics - */ - async getDatabaseMetrics() { - if (!this.knex) { - return null; - } - - try { - // Get connection info - const connections = await this.knex.raw(` - SELECT count(*) as total, - count(*) FILTER (WHERE state = 'active') as active, - count(*) FILTER (WHERE state = 'idle') as idle - FROM pg_stat_activity - WHERE datname = current_database() - `); - - // Get database size - const size = await this.knex.raw(` - SELECT pg_size_pretty(pg_database_size(current_database())) as size - `); - - return { - connections: connections.rows[0], - size: size.rows[0].size, - timestamp: new Date().toISOString() - }; - - } catch (error) { - return { - error: error.message - }; - } - } -} - -module.exports = DatabaseValidator; \ No newline at end of file diff --git a/scripts/debug-database.js b/scripts/debug-database.js deleted file mode 100755 index 126d0cb..0000000 --- a/scripts/debug-database.js +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/env node - -/** - * Comprehensive Database Debugging Tool - * - * This tool provides detailed database diagnostics and troubleshooting - * capabilities for the Shattered Void MMO. - */ - -require('dotenv').config(); -const DatabaseValidator = require('./database-validator'); - -// Color codes for console output -const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m', - white: '\x1b[37m' -}; - -function log(level, message) { - let colorCode = colors.white; - let prefix = 'INFO'; - - switch (level) { - case 'error': - colorCode = colors.red; - prefix = 'ERROR'; - break; - case 'warn': - colorCode = colors.yellow; - prefix = 'WARN'; - break; - case 'success': - colorCode = colors.green; - prefix = 'SUCCESS'; - break; - case 'info': - colorCode = colors.cyan; - prefix = 'INFO'; - break; - case 'debug': - colorCode = colors.magenta; - prefix = 'DEBUG'; - break; - } - - console.log(`${colors.bright}[${prefix}]${colors.reset} ${colorCode}${message}${colors.reset}`); -} - -function displayBanner() { - const banner = ` -${colors.cyan}╔═══════════════════════════════════════════════════════════════╗ -║ ║ -║ ${colors.bright}DATABASE DEBUGGING TOOL${colors.reset}${colors.cyan} ║ -║ ${colors.white}Comprehensive Database Diagnostics${colors.reset}${colors.cyan} ║ -║ ║ -╚═══════════════════════════════════════════════════════════════╝${colors.reset} -`; - console.log(banner); -} - -async function runComprehensiveCheck() { - try { - displayBanner(); - - log('info', 'Starting comprehensive database diagnostics...'); - - const validator = new DatabaseValidator(); - const results = await validator.validateDatabase(); - - // Display results in organized sections - console.log('\n' + colors.bright + '='.repeat(60) + colors.reset); - console.log(colors.bright + 'DATABASE VALIDATION RESULTS' + colors.reset); - console.log(colors.bright + '='.repeat(60) + colors.reset); - - // Overall Status - const overallStatus = results.success ? - `${colors.green}✅ PASSED${colors.reset}` : - `${colors.red}❌ FAILED${colors.reset}`; - console.log(`\nOverall Status: ${overallStatus}`); - console.log(`Validation Duration: ${results.duration}ms\n`); - - // Connectivity Check - console.log(colors.cyan + '📡 CONNECTIVITY CHECK' + colors.reset); - if (results.connectivity?.success) { - log('success', 'Database connection established'); - console.log(` Database: ${results.connectivity.database}`); - console.log(` Host: ${results.connectivity.host}:${results.connectivity.port}`); - console.log(` Version: ${results.connectivity.version}`); - console.log(` Size: ${results.connectivity.size}`); - console.log(` SSL: ${results.connectivity.ssl}`); - console.log(` Pool: ${results.connectivity.pool.used}/${results.connectivity.pool.max} connections used`); - } else { - log('error', `Connection failed: ${results.connectivity?.error}`); - if (results.connectivity?.troubleshooting) { - console.log(colors.yellow + ' Troubleshooting tips:' + colors.reset); - results.connectivity.troubleshooting.forEach(tip => - console.log(` - ${tip}`) - ); - } - } - - // Migration Check - console.log('\n' + colors.cyan + '📦 MIGRATION STATUS' + colors.reset); - if (results.migrations?.success) { - log('success', 'All migrations are up to date'); - console.log(` Current Batch: ${results.migrations.currentBatch}`); - console.log(` Completed: ${results.migrations.completed} migrations`); - console.log(` Pending: ${results.migrations.pending} migrations`); - - if (results.migrations.autoRan) { - log('info', `Auto-ran ${results.migrations.autoRan} pending migrations`); - } - } else { - log('error', `Migration check failed: ${results.migrations?.error}`); - } - - // Schema Check - console.log('\n' + colors.cyan + '🗂️ SCHEMA VALIDATION' + colors.reset); - if (results.schema?.success) { - log('success', 'All required tables exist'); - console.log(` Coverage: ${results.schema.coverage}`); - console.log(` Total Tables: ${results.schema.totalTables}`); - - // Table details - console.log('\n Table Details:'); - Object.entries(results.schema.tables).forEach(([tableName, info]) => { - if (info.exists) { - console.log(` ✅ ${tableName} (${info.columns} columns, ${info.rows} rows)`); - } else { - console.log(` ❌ ${tableName} - ${info.error}`); - } - }); - - // Optional tables if available - if (results.schema.optionalTables) { - console.log('\n Optional Tables:'); - Object.entries(results.schema.optionalTables).forEach(([tableName, info]) => { - console.log(` 📦 ${tableName} (${info.columns} columns, ${info.rows} rows)`); - }); - } - } else { - log('error', 'Schema validation failed'); - if (results.schema?.missingTables?.length > 0) { - console.log(` Missing tables: ${results.schema.missingTables.join(', ')}`); - } - if (results.schema?.troubleshooting) { - console.log(colors.yellow + ' Troubleshooting tips:' + colors.reset); - results.schema.troubleshooting.forEach(tip => - console.log(` - ${tip}`) - ); - } - } - - // Seed Data Check - console.log('\n' + colors.cyan + '🌱 SEED DATA STATUS' + colors.reset); - if (results.seeds?.success) { - log('success', results.seeds.message); - - if (results.seeds.autoSeeded) { - log('info', 'Seed data was automatically populated'); - } - - Object.entries(results.seeds.checks).forEach(([checkName, check]) => { - if (check.seeded) { - console.log(` ✅ ${checkName}: ${check.count || 'OK'}`); - } else { - console.log(` ❌ ${checkName}: ${check.error || 'Not seeded'}`); - } - }); - } else { - log('error', `Seed data check failed: ${results.seeds?.error}`); - } - - // Integrity Check - console.log('\n' + colors.cyan + '🔒 DATA INTEGRITY' + colors.reset); - if (results.integrity?.success) { - log('success', results.integrity.summary); - - results.integrity.checks.forEach(check => { - if (check.passed) { - console.log(` ✅ ${check.name}: ${check.message}`); - } else { - console.log(` ❌ ${check.name}: ${check.error || 'Failed'}`); - if (check.violations?.length > 0) { - check.violations.forEach(violation => - console.log(` - ${violation}`) - ); - } - } - }); - } else { - log('error', `Integrity check failed: ${results.integrity?.error}`); - } - - // Final Summary - console.log('\n' + colors.bright + '='.repeat(60) + colors.reset); - console.log(colors.bright + 'DEBUGGING SUMMARY' + colors.reset); - console.log(colors.bright + '='.repeat(60) + colors.reset); - - if (results.success) { - log('success', '🎉 All database checks passed! Your database is ready.'); - } else { - log('error', '❌ Database validation failed. Please review the issues above.'); - - // Provide actionable steps - console.log('\n' + colors.yellow + 'Recommended Actions:' + colors.reset); - - if (!results.connectivity?.success) { - console.log('1. Fix database connectivity issues first'); - } - - if (!results.migrations?.success) { - console.log('2. Run database migrations: npm run db:migrate'); - } - - if (!results.schema?.success) { - console.log('3. Ensure all required tables exist by running migrations'); - } - - if (!results.seeds?.success) { - console.log('4. Populate seed data: npm run db:seed'); - } - - if (!results.integrity?.success) { - console.log('5. Review and fix data integrity issues'); - } - } - - console.log(''); - - } catch (error) { - log('error', `Debugging tool failed: ${error.message}`); - console.error(error.stack); - process.exit(1); - } -} - -// Command line interface -const command = process.argv[2]; - -switch (command) { - case 'check': - case undefined: - runComprehensiveCheck(); - break; - case 'help': - console.log(` -Database Debugging Tool - -Usage: - node scripts/debug-database.js [command] - -Commands: - check (default) Run comprehensive database diagnostics - help Show this help message - -Examples: - node scripts/debug-database.js - node scripts/debug-database.js check -`); - break; - default: - log('error', `Unknown command: ${command}`); - log('info', 'Use "help" for available commands'); - process.exit(1); -} diff --git a/scripts/health-monitor.js b/scripts/health-monitor.js deleted file mode 100644 index 34f4d00..0000000 --- a/scripts/health-monitor.js +++ /dev/null @@ -1,506 +0,0 @@ -/** - * Shattered Void MMO - Health Monitoring System - * - * This module provides comprehensive health monitoring for all game services, - * including real-time status checks, performance metrics, and alerting. - */ - -const http = require('http'); -const { EventEmitter } = require('events'); -const os = require('os'); - -class HealthMonitor extends EventEmitter { - constructor(options = {}) { - super(); - - this.services = options.services || {}; - this.interval = options.interval || 30000; // 30 seconds - this.onHealthChange = options.onHealthChange || null; - this.timeout = options.timeout || 5000; // 5 seconds - - this.healthStatus = {}; - this.metrics = {}; - this.alertThresholds = { - responseTime: 5000, // 5 seconds - memoryUsage: 80, // 80% - cpuUsage: 90, // 90% - errorRate: 10 // 10% - }; - - this.monitoringInterval = null; - this.isRunning = false; - this.healthHistory = {}; - - // Initialize health status for all services - this.initializeHealthStatus(); - } - - /** - * Initialize health status tracking - */ - initializeHealthStatus() { - Object.keys(this.services).forEach(serviceName => { - this.healthStatus[serviceName] = { - status: 'unknown', - lastCheck: null, - responseTime: null, - consecutiveFailures: 0, - uptime: 0, - lastError: null - }; - - this.healthHistory[serviceName] = []; - }); - } - - /** - * Start health monitoring - */ - async start() { - if (this.isRunning) { - throw new Error('Health monitor is already running'); - } - - this.isRunning = true; - console.log(`🏥 Health monitoring started (interval: ${this.interval}ms)`); - - // Initial health check - await this.performHealthChecks(); - - // Start periodic monitoring - this.monitoringInterval = setInterval(async () => { - try { - await this.performHealthChecks(); - } catch (error) { - console.error('Health check error:', error); - } - }, this.interval); - - // Start system metrics monitoring - this.startSystemMetricsMonitoring(); - - this.emit('started'); - } - - /** - * Stop health monitoring - */ - stop() { - if (!this.isRunning) { - return; - } - - this.isRunning = false; - - if (this.monitoringInterval) { - clearInterval(this.monitoringInterval); - this.monitoringInterval = null; - } - - console.log('🏥 Health monitoring stopped'); - this.emit('stopped'); - } - - /** - * Perform health checks on all services - */ - async performHealthChecks() { - const checkPromises = Object.entries(this.services).map(([serviceName, serviceInfo]) => { - return this.checkServiceHealth(serviceName, serviceInfo); - }); - - await Promise.allSettled(checkPromises); - this.updateHealthSummary(); - } - - /** - * Check health of a specific service - */ - async checkServiceHealth(serviceName, serviceInfo) { - const startTime = Date.now(); - const previousStatus = this.healthStatus[serviceName].status; - - try { - let isHealthy = false; - let responseTime = null; - - // Different health check strategies based on service type - switch (serviceName) { - case 'backend': - isHealthy = await this.checkHttpService(serviceInfo.port, '/health'); - responseTime = Date.now() - startTime; - break; - - case 'frontend': - isHealthy = await this.checkHttpService(serviceInfo.port); - responseTime = Date.now() - startTime; - break; - - case 'database': - isHealthy = await this.checkDatabaseHealth(); - responseTime = Date.now() - startTime; - break; - - case 'redis': - isHealthy = await this.checkRedisHealth(); - responseTime = Date.now() - startTime; - break; - - default: - // For other services, assume healthy if they exist - isHealthy = true; - responseTime = Date.now() - startTime; - } - - // Update health status - const newStatus = isHealthy ? 'healthy' : 'unhealthy'; - this.updateServiceStatus(serviceName, { - status: newStatus, - lastCheck: new Date(), - responseTime, - consecutiveFailures: isHealthy ? 0 : this.healthStatus[serviceName].consecutiveFailures + 1, - lastError: null - }); - - // Emit health change event if status changed - if (previousStatus !== newStatus && this.onHealthChange) { - this.onHealthChange(serviceName, newStatus); - } - - } catch (error) { - const responseTime = Date.now() - startTime; - - this.updateServiceStatus(serviceName, { - status: 'unhealthy', - lastCheck: new Date(), - responseTime, - consecutiveFailures: this.healthStatus[serviceName].consecutiveFailures + 1, - lastError: error.message - }); - - // Emit health change event if status changed - if (previousStatus !== 'unhealthy' && this.onHealthChange) { - this.onHealthChange(serviceName, 'unhealthy'); - } - - console.error(`Health check failed for ${serviceName}:`, error.message); - } - } - - /** - * Check HTTP service health - */ - checkHttpService(port, path = '/') { - return new Promise((resolve, reject) => { - const options = { - hostname: 'localhost', - port: port, - path: path, - method: 'GET', - timeout: this.timeout - }; - - const req = http.request(options, (res) => { - // Consider 2xx and 3xx status codes as healthy - resolve(res.statusCode >= 200 && res.statusCode < 400); - }); - - req.on('error', (error) => { - reject(error); - }); - - req.on('timeout', () => { - req.destroy(); - reject(new Error('Request timeout')); - }); - - req.end(); - }); - } - - /** - * Check database health - */ - async checkDatabaseHealth() { - try { - // Try to get database connection from the app - const db = require('../src/database/connection'); - - // Simple query to check database connectivity - await db.raw('SELECT 1'); - return true; - } catch (error) { - return false; - } - } - - /** - * Check Redis health - */ - async checkRedisHealth() { - try { - // Skip if Redis is disabled - if (process.env.DISABLE_REDIS === 'true') { - return true; - } - - // Try to get Redis client from the app - const redisConfig = require('../src/config/redis'); - - if (!redisConfig.client) { - return false; - } - - // Simple ping to check Redis connectivity - await redisConfig.client.ping(); - return true; - } catch (error) { - return false; - } - } - - /** - * Update service status - */ - updateServiceStatus(serviceName, statusUpdate) { - this.healthStatus[serviceName] = { - ...this.healthStatus[serviceName], - ...statusUpdate - }; - - // Add to health history - this.addToHealthHistory(serviceName, statusUpdate); - - // Check for alerts - this.checkForAlerts(serviceName); - } - - /** - * Add health data to history - */ - addToHealthHistory(serviceName, statusData) { - const historyEntry = { - timestamp: Date.now(), - status: statusData.status, - responseTime: statusData.responseTime, - error: statusData.lastError - }; - - this.healthHistory[serviceName].push(historyEntry); - - // Keep only last 100 entries - if (this.healthHistory[serviceName].length > 100) { - this.healthHistory[serviceName] = this.healthHistory[serviceName].slice(-100); - } - } - - /** - * Check for health alerts - */ - checkForAlerts(serviceName) { - const health = this.healthStatus[serviceName]; - const alerts = []; - - // Check consecutive failures - if (health.consecutiveFailures >= 3) { - alerts.push({ - type: 'consecutive_failures', - message: `Service ${serviceName} has failed ${health.consecutiveFailures} consecutive times`, - severity: 'critical' - }); - } - - // Check response time - if (health.responseTime && health.responseTime > this.alertThresholds.responseTime) { - alerts.push({ - type: 'slow_response', - message: `Service ${serviceName} response time: ${health.responseTime}ms (threshold: ${this.alertThresholds.responseTime}ms)`, - severity: 'warning' - }); - } - - // Emit alerts - alerts.forEach(alert => { - this.emit('alert', serviceName, alert); - }); - } - - /** - * Start system metrics monitoring - */ - startSystemMetricsMonitoring() { - const updateSystemMetrics = () => { - const memUsage = process.memoryUsage(); - const cpuUsage = process.cpuUsage(); - const systemMem = { - total: os.totalmem(), - free: os.freemem() - }; - - this.metrics.system = { - timestamp: Date.now(), - memory: { - rss: memUsage.rss, - heapTotal: memUsage.heapTotal, - heapUsed: memUsage.heapUsed, - external: memUsage.external, - usage: Math.round((memUsage.heapUsed / memUsage.heapTotal) * 100) - }, - cpu: { - user: cpuUsage.user, - system: cpuUsage.system - }, - systemMemory: { - total: systemMem.total, - free: systemMem.free, - used: systemMem.total - systemMem.free, - usage: Math.round(((systemMem.total - systemMem.free) / systemMem.total) * 100) - }, - uptime: process.uptime(), - loadAverage: os.loadavg() - }; - - // Check for system alerts - this.checkSystemAlerts(); - }; - - // Update immediately - updateSystemMetrics(); - - // Update every 10 seconds - setInterval(updateSystemMetrics, 10000); - } - - /** - * Check for system-level alerts - */ - checkSystemAlerts() { - const metrics = this.metrics.system; - - if (!metrics) return; - - // Memory usage alert - if (metrics.memory.usage > this.alertThresholds.memoryUsage) { - this.emit('alert', 'system', { - type: 'high_memory_usage', - message: `High memory usage: ${metrics.memory.usage}% (threshold: ${this.alertThresholds.memoryUsage}%)`, - severity: 'warning' - }); - } - - // System memory alert - if (metrics.systemMemory.usage > this.alertThresholds.memoryUsage) { - this.emit('alert', 'system', { - type: 'high_system_memory', - message: `High system memory usage: ${metrics.systemMemory.usage}% (threshold: ${this.alertThresholds.memoryUsage}%)`, - severity: 'critical' - }); - } - } - - /** - * Update overall health summary - */ - updateHealthSummary() { - const services = Object.keys(this.healthStatus); - const healthyServices = services.filter(s => this.healthStatus[s].status === 'healthy'); - const unhealthyServices = services.filter(s => this.healthStatus[s].status === 'unhealthy'); - - this.metrics.summary = { - timestamp: Date.now(), - totalServices: services.length, - healthyServices: healthyServices.length, - unhealthyServices: unhealthyServices.length, - overallHealth: unhealthyServices.length === 0 ? 'healthy' : 'degraded' - }; - } - - /** - * Get current health status - */ - getHealthStatus() { - return { - services: this.healthStatus, - metrics: this.metrics, - summary: this.metrics.summary, - isRunning: this.isRunning - }; - } - - /** - * Get health history for a service - */ - getHealthHistory(serviceName) { - return this.healthHistory[serviceName] || []; - } - - /** - * Get service uptime - */ - getServiceUptime(serviceName) { - const history = this.healthHistory[serviceName]; - if (!history || history.length === 0) { - return 0; - } - - const now = Date.now(); - const oneDayAgo = now - (24 * 60 * 60 * 1000); - - const recentHistory = history.filter(entry => entry.timestamp > oneDayAgo); - - if (recentHistory.length === 0) { - return 0; - } - - const healthyCount = recentHistory.filter(entry => entry.status === 'healthy').length; - return Math.round((healthyCount / recentHistory.length) * 100); - } - - /** - * Generate health report - */ - generateHealthReport() { - const services = Object.keys(this.healthStatus); - const report = { - timestamp: new Date().toISOString(), - summary: this.metrics.summary, - services: {}, - systemMetrics: this.metrics.system, - alerts: [] - }; - - services.forEach(serviceName => { - const health = this.healthStatus[serviceName]; - const uptime = this.getServiceUptime(serviceName); - - report.services[serviceName] = { - status: health.status, - lastCheck: health.lastCheck, - responseTime: health.responseTime, - consecutiveFailures: health.consecutiveFailures, - uptime: `${uptime}%`, - lastError: health.lastError - }; - }); - - return report; - } - - /** - * Export health data for monitoring systems - */ - exportMetrics() { - return { - timestamp: Date.now(), - services: this.healthStatus, - system: this.metrics.system, - summary: this.metrics.summary, - uptime: Object.keys(this.healthStatus).reduce((acc, serviceName) => { - acc[serviceName] = this.getServiceUptime(serviceName); - return acc; - }, {}) - }; - } -} - -module.exports = HealthMonitor; \ No newline at end of file diff --git a/scripts/setup-combat.js b/scripts/setup-combat.js deleted file mode 100644 index 7a8fc99..0000000 --- a/scripts/setup-combat.js +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/env node - -/** - * Combat System Setup Script - * Initializes combat configurations and sample data - */ - -const db = require('../src/database/connection'); -const logger = require('../src/utils/logger'); - -async function setupCombatSystem() { - try { - console.log('🚀 Setting up combat system...'); - - // Insert default combat configurations - console.log('📝 Adding default combat configurations...'); - - const existingConfigs = await db('combat_configurations').select('id'); - if (existingConfigs.length === 0) { - await db('combat_configurations').insert([ - { - config_name: 'instant_combat', - combat_type: 'instant', - config_data: JSON.stringify({ - auto_resolve: true, - preparation_time: 5, - damage_variance: 0.15, - experience_gain: 1.0, - casualty_rate_min: 0.05, - casualty_rate_max: 0.75, - loot_multiplier: 1.0, - spectator_limit: 50, - priority: 100 - }), - description: 'Standard instant combat resolution with quick results', - is_active: true, - created_at: new Date(), - updated_at: new Date() - }, - { - config_name: 'turn_based_combat', - combat_type: 'turn_based', - config_data: JSON.stringify({ - auto_resolve: true, - preparation_time: 10, - max_rounds: 15, - round_duration: 3, - damage_variance: 0.2, - experience_gain: 1.5, - casualty_rate_min: 0.1, - casualty_rate_max: 0.8, - loot_multiplier: 1.2, - spectator_limit: 100, - priority: 150 - }), - description: 'Detailed turn-based combat with round-by-round resolution', - is_active: true, - created_at: new Date(), - updated_at: new Date() - }, - { - config_name: 'tactical_combat', - combat_type: 'tactical', - config_data: JSON.stringify({ - auto_resolve: true, - preparation_time: 15, - max_rounds: 20, - round_duration: 4, - damage_variance: 0.25, - experience_gain: 2.0, - casualty_rate_min: 0.15, - casualty_rate_max: 0.85, - loot_multiplier: 1.5, - spectator_limit: 200, - priority: 200 - }), - description: 'Advanced tactical combat with positioning and formations', - is_active: true, - created_at: new Date(), - updated_at: new Date() - } - ]); - - console.log('✅ Combat configurations added successfully'); - } else { - console.log('ℹ️ Combat configurations already exist, skipping...'); - } - - // Update combat types table with default plugin reference - console.log('📝 Updating combat types...'); - - const existingCombatTypes = await db('combat_types').select('id'); - if (existingCombatTypes.length === 0) { - await db('combat_types').insert([ - { - name: 'instant_resolution', - description: 'Basic instant combat resolution with detailed logs', - plugin_name: 'instant_combat', - config: JSON.stringify({ - calculate_experience: true, - detailed_logs: true, - enable_spectators: true - }), - is_active: true - }, - { - name: 'turn_based_resolution', - description: 'Turn-based combat with round-by-round progression', - plugin_name: 'turn_based_combat', - config: JSON.stringify({ - calculate_experience: true, - detailed_logs: true, - enable_spectators: true, - show_round_details: true - }), - is_active: true - }, - { - name: 'tactical_resolution', - description: 'Advanced tactical combat with formations and positioning', - plugin_name: 'tactical_combat', - config: JSON.stringify({ - calculate_experience: true, - detailed_logs: true, - enable_spectators: true, - enable_formations: true, - enable_positioning: true - }), - is_active: true - } - ]); - - console.log('✅ Combat types added successfully'); - } else { - console.log('ℹ️ Combat types already exist, skipping...'); - } - - // Ensure combat plugins are properly registered - console.log('📝 Checking combat plugins...'); - - const combatPlugins = await db('plugins').where('plugin_type', 'combat'); - const pluginNames = combatPlugins.map(p => p.name); - - const requiredPlugins = [ - { - name: 'instant_combat', - version: '1.0.0', - description: 'Basic instant combat resolution system', - plugin_type: 'combat', - is_active: true, - config: JSON.stringify({ - damage_variance: 0.15, - experience_gain: 1.0 - }), - dependencies: JSON.stringify([]), - hooks: JSON.stringify(['pre_combat', 'post_combat', 'damage_calculation']) - }, - { - name: 'turn_based_combat', - version: '1.0.0', - description: 'Turn-based combat resolution system with detailed rounds', - plugin_type: 'combat', - is_active: true, - config: JSON.stringify({ - max_rounds: 15, - damage_variance: 0.2, - experience_gain: 1.5 - }), - dependencies: JSON.stringify([]), - hooks: JSON.stringify(['pre_combat', 'post_combat', 'round_start', 'round_end', 'damage_calculation']) - }, - { - name: 'tactical_combat', - version: '1.0.0', - description: 'Advanced tactical combat with formations and positioning', - plugin_type: 'combat', - is_active: true, - config: JSON.stringify({ - enable_formations: true, - enable_positioning: true, - damage_variance: 0.25, - experience_gain: 2.0 - }), - dependencies: JSON.stringify([]), - hooks: JSON.stringify(['pre_combat', 'post_combat', 'formation_change', 'position_update', 'damage_calculation']) - } - ]; - - for (const plugin of requiredPlugins) { - if (!pluginNames.includes(plugin.name)) { - await db('plugins').insert(plugin); - console.log(`✅ Added combat plugin: ${plugin.name}`); - } else { - console.log(`ℹ️ Combat plugin ${plugin.name} already exists`); - } - } - - // Add sample ship designs if none exist (for testing) - console.log('📝 Checking for sample ship designs...'); - - const existingDesigns = await db('ship_designs').where('is_public', true); - if (existingDesigns.length === 0) { - await db('ship_designs').insert([ - { - name: 'Basic Fighter', - ship_class: 'fighter', - hull_type: 'light', - components: JSON.stringify({ - weapons: ['laser_cannon'], - shields: ['basic_shield'], - engines: ['ion_drive'] - }), - stats: JSON.stringify({ - hp: 75, - attack: 12, - defense: 8, - speed: 6 - }), - cost: JSON.stringify({ - scrap: 80, - energy: 40 - }), - build_time: 20, - is_public: true, - is_active: true, - hull_points: 75, - shield_points: 20, - armor_points: 5, - attack_power: 12, - attack_speed: 1.2, - movement_speed: 6, - cargo_capacity: 0, - special_abilities: JSON.stringify([]), - damage_resistances: JSON.stringify({}), - created_at: new Date(), - updated_at: new Date() - }, - { - name: 'Heavy Cruiser', - ship_class: 'cruiser', - hull_type: 'heavy', - components: JSON.stringify({ - weapons: ['plasma_cannon', 'missile_launcher'], - shields: ['reinforced_shield'], - engines: ['fusion_drive'] - }), - stats: JSON.stringify({ - hp: 200, - attack: 25, - defense: 18, - speed: 3 - }), - cost: JSON.stringify({ - scrap: 300, - energy: 180, - rare_elements: 5 - }), - build_time: 120, - is_public: true, - is_active: true, - hull_points: 200, - shield_points: 60, - armor_points: 25, - attack_power: 25, - attack_speed: 0.8, - movement_speed: 3, - cargo_capacity: 50, - special_abilities: JSON.stringify(['heavy_armor', 'shield_boost']), - damage_resistances: JSON.stringify({ - kinetic: 0.1, - energy: 0.05 - }), - created_at: new Date(), - updated_at: new Date() - } - ]); - - console.log('✅ Added sample ship designs'); - } else { - console.log('ℹ️ Ship designs already exist, skipping...'); - } - - console.log('🎉 Combat system setup completed successfully!'); - console.log(''); - console.log('Combat system is now ready for use with:'); - console.log('- 3 combat configurations (instant, turn-based, tactical)'); - console.log('- 3 combat resolution plugins'); - console.log('- Sample ship designs for testing'); - console.log(''); - console.log('You can now:'); - console.log('• Create fleets and initiate combat via /api/combat/initiate'); - console.log('• View combat history via /api/combat/history'); - console.log('• Manage combat system via admin endpoints'); - - } catch (error) { - console.error('❌ Combat system setup failed:', error); - throw error; - } -} - -// Main execution -if (require.main === module) { - setupCombatSystem() - .then(() => { - console.log('✨ Setup completed successfully'); - process.exit(0); - }) - .catch(error => { - console.error('💥 Setup failed:', error); - process.exit(1); - }); -} - -module.exports = { setupCombatSystem }; \ No newline at end of file diff --git a/scripts/startup-checks.js b/scripts/startup-checks.js deleted file mode 100644 index f3fe607..0000000 --- a/scripts/startup-checks.js +++ /dev/null @@ -1,591 +0,0 @@ -/** - * Shattered Void MMO - Comprehensive Startup Checks - * - * This module performs thorough pre-flight checks to ensure all dependencies, - * configurations, and system requirements are met before starting the game. - */ - -const fs = require('fs').promises; -const path = require('path'); -const { exec } = require('child_process'); -const { promisify } = require('util'); -const net = require('net'); - -const execAsync = promisify(exec); - -class StartupChecks { - constructor() { - this.checks = []; - this.results = {}; - } - - /** - * Add a check to the validation suite - */ - addCheck(name, checkFunction, required = true) { - this.checks.push({ - name, - function: checkFunction, - required - }); - } - - /** - * Run all registered checks - */ - async runAllChecks() { - const startTime = Date.now(); - const results = { - success: true, - checks: {}, - failures: [], - duration: 0 - }; - - // Register all standard checks - this.registerStandardChecks(); - - console.log(`🔍 Running ${this.checks.length} startup checks...`); - - for (const check of this.checks) { - try { - console.log(` ⏳ ${check.name}...`); - const checkResult = await check.function(); - - results.checks[check.name] = { - success: true, - required: check.required, - details: checkResult - }; - - console.log(` ✅ ${check.name}`); - } catch (error) { - const failure = { - name: check.name, - required: check.required, - error: error.message - }; - - results.checks[check.name] = { - success: false, - required: check.required, - error: error.message - }; - - results.failures.push(failure); - - if (check.required) { - results.success = false; - console.log(` ❌ ${check.name}: ${error.message}`); - } else { - console.log(` ⚠️ ${check.name}: ${error.message} (optional)`); - } - } - } - - results.duration = Date.now() - startTime; - return results; - } - - /** - * Register all standard checks - */ - registerStandardChecks() { - // Node.js version check - this.addCheck('Node.js Version', this.checkNodeVersion, true); - - // NPM availability - this.addCheck('NPM Availability', this.checkNpmAvailability, true); - - // Environment configuration - this.addCheck('Environment Configuration', this.checkEnvironmentConfig, true); - - // Required directories - this.addCheck('Directory Structure', this.checkDirectoryStructure, true); - - // Package dependencies - this.addCheck('Package Dependencies', this.checkPackageDependencies, true); - - // Port availability - this.addCheck('Port Availability', this.checkPortAvailability, true); - - // Database configuration - this.addCheck('Database Configuration', this.checkDatabaseConfig, true); - - // Redis configuration - this.addCheck('Redis Configuration', this.checkRedisConfig, false); - - // Log directories - this.addCheck('Log Directories', this.checkLogDirectories, true); - - // Frontend availability - this.addCheck('Frontend Dependencies', this.checkFrontendDependencies, false); - - // Memory availability - this.addCheck('System Memory', this.checkSystemMemory, true); - - // Disk space - this.addCheck('Disk Space', this.checkDiskSpace, true); - - // File permissions - this.addCheck('File Permissions', this.checkFilePermissions, true); - } - - /** - * Check Node.js version requirements - */ - async checkNodeVersion() { - const requiredMajor = 18; - const currentVersion = process.version; - const major = parseInt(currentVersion.slice(1).split('.')[0]); - - if (major < requiredMajor) { - throw new Error(`Node.js ${requiredMajor}+ required, found ${currentVersion}`); - } - - return { - current: currentVersion, - required: `>=${requiredMajor}.0.0`, - valid: true - }; - } - - /** - * Check NPM availability - */ - async checkNpmAvailability() { - try { - const { stdout } = await execAsync('npm --version'); - const version = stdout.trim(); - - return { - version, - available: true - }; - } catch (error) { - throw new Error('NPM not found in PATH'); - } - } - - /** - * Check environment configuration - */ - async checkEnvironmentConfig() { - const envFile = path.join(process.cwd(), '.env'); - const config = { - hasEnvFile: false, - requiredVars: [], - missingVars: [], - warnings: [] - }; - - // Check for .env file - try { - await fs.access(envFile); - config.hasEnvFile = true; - } catch { - config.warnings.push('No .env file found, using defaults'); - } - - // Required environment variables (with defaults) - const requiredVars = [ - { name: 'NODE_ENV', default: 'development' }, - { name: 'PORT', default: '3000' }, - { name: 'DB_HOST', default: 'localhost' }, - { name: 'DB_PORT', default: '5432' }, - { name: 'DB_NAME', default: 'shattered_void_dev' }, - { name: 'DB_USER', default: 'postgres' } - ]; - - for (const varConfig of requiredVars) { - const value = process.env[varConfig.name]; - if (!value) { - config.missingVars.push({ - name: varConfig.name, - default: varConfig.default - }); - } else { - config.requiredVars.push({ - name: varConfig.name, - value: varConfig.name.includes('PASSWORD') ? '[HIDDEN]' : value - }); - } - } - - return config; - } - - /** - * Check directory structure - */ - async checkDirectoryStructure() { - const requiredDirs = [ - 'src', - 'src/controllers', - 'src/services', - 'src/routes', - 'src/database', - 'src/database/migrations', - 'config', - 'scripts' - ]; - - const optionalDirs = [ - 'frontend', - 'frontend/src', - 'frontend/dist', - 'logs', - 'tests' - ]; - - const results = { - required: [], - optional: [], - missing: [] - }; - - // Check required directories - for (const dir of requiredDirs) { - try { - const stats = await fs.stat(dir); - if (stats.isDirectory()) { - results.required.push(dir); - } else { - results.missing.push(dir); - } - } catch { - results.missing.push(dir); - } - } - - // Check optional directories - for (const dir of optionalDirs) { - try { - const stats = await fs.stat(dir); - if (stats.isDirectory()) { - results.optional.push(dir); - } - } catch { - // Optional directories are not reported as missing - } - } - - if (results.missing.length > 0) { - throw new Error(`Missing required directories: ${results.missing.join(', ')}`); - } - - return results; - } - - /** - * Check package dependencies - */ - async checkPackageDependencies() { - const packageJsonPath = path.join(process.cwd(), 'package.json'); - const nodeModulesPath = path.join(process.cwd(), 'node_modules'); - - try { - // Check package.json exists - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); - - // Check node_modules exists - await fs.access(nodeModulesPath); - - // Check critical dependencies - const criticalDeps = [ - 'express', - 'pg', - 'knex', - 'winston', - 'dotenv', - 'socket.io' - ]; - - const missing = []; - for (const dep of criticalDeps) { - try { - await fs.access(path.join(nodeModulesPath, dep)); - } catch { - missing.push(dep); - } - } - - if (missing.length > 0) { - throw new Error(`Missing critical dependencies: ${missing.join(', ')}`); - } - - return { - packageJson: packageJson.name, - version: packageJson.version, - dependencies: Object.keys(packageJson.dependencies || {}).length, - devDependencies: Object.keys(packageJson.devDependencies || {}).length, - criticalDeps: criticalDeps.length - }; - } catch (error) { - throw new Error(`Package validation failed: ${error.message}`); - } - } - - /** - * Check port availability - */ - async checkPortAvailability() { - const backendPort = process.env.PORT || 3000; - const frontendPort = process.env.FRONTEND_PORT || 5173; - - const checkPort = (port) => { - return new Promise((resolve, reject) => { - const server = net.createServer(); - - server.listen(port, (err) => { - if (err) { - reject(new Error(`Port ${port} is in use`)); - } else { - server.close(() => resolve(port)); - } - }); - - server.on('error', (err) => { - reject(new Error(`Port ${port} is in use`)); - }); - }); - }; - - const results = { - backend: await checkPort(backendPort), - frontend: null - }; - - // Only check frontend port if frontend is enabled - if (process.env.ENABLE_FRONTEND !== 'false') { - try { - results.frontend = await checkPort(frontendPort); - } catch (error) { - // Frontend port check is not critical - results.frontendError = error.message; - } - } - - return results; - } - - /** - * Check database configuration - */ - async checkDatabaseConfig() { - const config = { - host: process.env.DB_HOST || 'localhost', - port: process.env.DB_PORT || 5432, - database: process.env.DB_NAME || 'shattered_void_dev', - user: process.env.DB_USER || 'postgres' - }; - - // Check if database connection parameters are reasonable - if (!config.host || !config.port || !config.database || !config.user) { - throw new Error('Incomplete database configuration'); - } - - // Validate port number - const port = parseInt(config.port); - if (isNaN(port) || port < 1 || port > 65535) { - throw new Error(`Invalid database port: ${config.port}`); - } - - return { - host: config.host, - port: config.port, - database: config.database, - user: config.user, - configured: true - }; - } - - /** - * Check Redis configuration (optional) - */ - async checkRedisConfig() { - const config = { - host: process.env.REDIS_HOST || 'localhost', - port: process.env.REDIS_PORT || 6379, - enabled: process.env.DISABLE_REDIS !== 'true' - }; - - if (!config.enabled) { - return { - enabled: false, - message: 'Redis disabled by configuration' - }; - } - - // Validate port number - const port = parseInt(config.port); - if (isNaN(port) || port < 1 || port > 65535) { - throw new Error(`Invalid Redis port: ${config.port}`); - } - - return { - host: config.host, - port: config.port, - enabled: true - }; - } - - /** - * Check log directories - */ - async checkLogDirectories() { - const logDir = path.join(process.cwd(), 'logs'); - - try { - // Check if logs directory exists - await fs.access(logDir); - - // Check if it's writable - await fs.access(logDir, fs.constants.W_OK); - - return { - directory: logDir, - exists: true, - writable: true - }; - } catch { - // Create logs directory if it doesn't exist - try { - await fs.mkdir(logDir, { recursive: true }); - return { - directory: logDir, - exists: true, - writable: true, - created: true - }; - } catch (error) { - throw new Error(`Cannot create logs directory: ${error.message}`); - } - } - } - - /** - * Check frontend dependencies (optional) - */ - async checkFrontendDependencies() { - const frontendDir = path.join(process.cwd(), 'frontend'); - - try { - // Check if frontend directory exists - await fs.access(frontendDir); - - // Check package.json - const packageJsonPath = path.join(frontendDir, 'package.json'); - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); - - // Check node_modules - const nodeModulesPath = path.join(frontendDir, 'node_modules'); - await fs.access(nodeModulesPath); - - return { - directory: frontendDir, - name: packageJson.name, - version: packageJson.version, - dependencies: Object.keys(packageJson.dependencies || {}).length, - hasNodeModules: true - }; - } catch (error) { - throw new Error(`Frontend not available: ${error.message}`); - } - } - - /** - * Check system memory - */ - async checkSystemMemory() { - const totalMemory = require('os').totalmem(); - const freeMemory = require('os').freemem(); - const usedMemory = totalMemory - freeMemory; - - const totalGB = totalMemory / (1024 * 1024 * 1024); - const freeGB = freeMemory / (1024 * 1024 * 1024); - const usedGB = usedMemory / (1024 * 1024 * 1024); - - // Minimum 1GB free memory recommended - if (freeGB < 1) { - throw new Error(`Low memory: ${freeGB.toFixed(2)}GB free, 1GB+ recommended`); - } - - return { - total: `${totalGB.toFixed(2)}GB`, - used: `${usedGB.toFixed(2)}GB`, - free: `${freeGB.toFixed(2)}GB`, - usage: `${((usedGB / totalGB) * 100).toFixed(1)}%` - }; - } - - /** - * Check disk space - */ - async checkDiskSpace() { - try { - const { stdout } = await execAsync('df -h .'); - const lines = stdout.trim().split('\n'); - const data = lines[1].split(/\s+/); - - const size = data[1]; - const used = data[2]; - const available = data[3]; - const usage = data[4]; - - // Extract numeric percentage - const usagePercent = parseInt(usage.replace('%', '')); - - // Warn if disk usage is over 90% - if (usagePercent > 90) { - throw new Error(`High disk usage: ${usage} used, <10% available`); - } - - return { - size, - used, - available, - usage: `${usagePercent}%` - }; - } catch (error) { - // Fallback for non-Unix systems or when df is not available - return { - message: 'Disk space check not available on this system', - available: true - }; - } - } - - /** - * Check file permissions - */ - async checkFilePermissions() { - const criticalFiles = [ - 'src/server.js', - 'package.json', - 'knexfile.js' - ]; - - const results = { - readable: [], - unreadable: [] - }; - - for (const file of criticalFiles) { - try { - await fs.access(file, fs.constants.R_OK); - results.readable.push(file); - } catch { - results.unreadable.push(file); - } - } - - if (results.unreadable.length > 0) { - throw new Error(`Cannot read critical files: ${results.unreadable.join(', ')}`); - } - - return results; - } -} - -module.exports = StartupChecks; \ No newline at end of file diff --git a/src/app.js b/src/app.js index 978c470..6d0d3b0 100644 --- a/src/app.js +++ b/src/app.js @@ -26,94 +26,94 @@ const routes = require('./routes'); * @returns {Object} Configured Express app */ function createApp() { - const app = express(); - const NODE_ENV = process.env.NODE_ENV || 'development'; + const app = express(); + const NODE_ENV = process.env.NODE_ENV || 'development'; - // Add correlation ID to all requests for tracing - app.use((req, res, next) => { - req.correlationId = uuidv4(); - res.set('X-Correlation-ID', req.correlationId); - next(); - }); - - // Security middleware - app.use(helmet({ - contentSecurityPolicy: NODE_ENV === 'production' ? undefined : false, - crossOriginEmbedderPolicy: false, // Allow WebSocket connections - })); - - // CORS middleware - app.use(corsMiddleware); - - // Compression middleware - app.use(compression()); - - // Body parsing middleware - app.use(express.json({ - limit: process.env.REQUEST_SIZE_LIMIT || '10mb', - verify: (req, res, buf) => { - // Store raw body for webhook verification if needed - req.rawBody = buf; - }, - })); - app.use(express.urlencoded({ - extended: true, - limit: process.env.REQUEST_SIZE_LIMIT || '10mb', - })); - - // Cookie parsing middleware - app.use(cookieParser()); - - // Request logging middleware - app.use(requestLogger); - - // Rate limiting middleware - app.use(rateLimiters.global); - - // Health check endpoint (before other routes) - app.get('/health', (req, res) => { - const healthData = { - status: 'healthy', - timestamp: new Date().toISOString(), - version: process.env.npm_package_version || '0.1.0', - environment: NODE_ENV, - uptime: process.uptime(), - memory: { - used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), - total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), - rss: Math.round(process.memoryUsage().rss / 1024 / 1024), - }, - }; - - res.status(200).json(healthData); - }); - - // API routes - app.use('/', routes); - - // 404 handler for unmatched routes - app.use('*', (req, res) => { - logger.warn('Route not found', { - correlationId: req.correlationId, - method: req.method, - url: req.originalUrl, - ip: req.ip, - userAgent: req.get('User-Agent'), + // Add correlation ID to all requests for tracing + app.use((req, res, next) => { + req.correlationId = uuidv4(); + res.set('X-Correlation-ID', req.correlationId); + next(); }); - res.status(404).json({ - error: 'Not Found', - message: 'The requested resource was not found', - path: req.originalUrl, - timestamp: new Date().toISOString(), - correlationId: req.correlationId, + // Security middleware + app.use(helmet({ + contentSecurityPolicy: NODE_ENV === 'production' ? undefined : false, + crossOriginEmbedderPolicy: false, // Allow WebSocket connections + })); + + // CORS middleware + app.use(corsMiddleware); + + // Compression middleware + app.use(compression()); + + // Body parsing middleware + app.use(express.json({ + limit: process.env.REQUEST_SIZE_LIMIT || '10mb', + verify: (req, res, buf) => { + // Store raw body for webhook verification if needed + req.rawBody = buf; + } + })); + app.use(express.urlencoded({ + extended: true, + limit: process.env.REQUEST_SIZE_LIMIT || '10mb' + })); + + // Cookie parsing middleware + app.use(cookieParser()); + + // Request logging middleware + app.use(requestLogger); + + // Rate limiting middleware + app.use(rateLimiters.global); + + // Health check endpoint (before other routes) + app.get('/health', (req, res) => { + const healthData = { + status: 'healthy', + timestamp: new Date().toISOString(), + version: process.env.npm_package_version || '0.1.0', + environment: NODE_ENV, + uptime: process.uptime(), + memory: { + used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), + total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), + rss: Math.round(process.memoryUsage().rss / 1024 / 1024) + } + }; + + res.status(200).json(healthData); }); - }); - // Global error handler (must be last) - app.use(errorHandler); + // API routes + app.use('/', routes); - return app; + // 404 handler for unmatched routes + app.use('*', (req, res) => { + logger.warn('Route not found', { + correlationId: req.correlationId, + method: req.method, + url: req.originalUrl, + ip: req.ip, + userAgent: req.get('User-Agent') + }); + + res.status(404).json({ + error: 'Not Found', + message: 'The requested resource was not found', + path: req.originalUrl, + timestamp: new Date().toISOString(), + correlationId: req.correlationId + }); + }); + + // Global error handler (must be last) + app.use(errorHandler); + + return app; } -module.exports = createApp; +module.exports = createApp; \ No newline at end of file diff --git a/src/config/email.js b/src/config/email.js deleted file mode 100644 index ede1840..0000000 --- a/src/config/email.js +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Email Configuration - * Centralized email service configuration with environment-based setup - */ - -const logger = require('../utils/logger'); - -/** - * Email service configuration based on environment - */ -const emailConfig = { - // Development configuration (console logging) - development: { - provider: 'mock', - settings: { - host: 'localhost', - port: 1025, - secure: false, - logger: true, - }, - }, - - // Production configuration (actual SMTP) - production: { - provider: 'smtp', - settings: { - host: process.env.SMTP_HOST, - port: parseInt(process.env.SMTP_PORT) || 587, - secure: process.env.SMTP_SECURE === 'true', - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, - }, - }, - }, - - // Test configuration (nodemailer test accounts) - test: { - provider: 'test', - settings: { - host: 'smtp.ethereal.email', - port: 587, - secure: false, - auth: { - user: 'ethereal.user@ethereal.email', - pass: 'ethereal.pass', - }, - }, - }, -}; - -/** - * Get current email configuration based on environment - * @returns {Object} Email configuration - */ -function getEmailConfig() { - const env = process.env.NODE_ENV || 'development'; - const config = emailConfig[env] || emailConfig.development; - - logger.info('Email configuration loaded', { - environment: env, - provider: config.provider, - host: config.settings.host, - port: config.settings.port, - }); - - return config; -} - -/** - * Validate email configuration - * @param {Object} config - Email configuration to validate - * @returns {Object} Validation result - */ -function validateEmailConfig(config) { - const errors = []; - - if (!config) { - errors.push('Email configuration is missing'); - return { isValid: false, errors }; - } - - if (!config.settings) { - errors.push('Email settings are missing'); - return { isValid: false, errors }; - } - - // Skip validation for mock/development mode - if (config.provider === 'mock') { - return { isValid: true, errors: [] }; - } - - const { settings } = config; - - if (!settings.host) { - errors.push('SMTP host is required'); - } - - if (!settings.port) { - errors.push('SMTP port is required'); - } - - if (config.provider === 'smtp' && (!settings.auth || !settings.auth.user || !settings.auth.pass)) { - errors.push('SMTP authentication credentials are required for production'); - } - - return { - isValid: errors.length === 0, - errors, - }; -} - -/** - * Email templates configuration - */ -const emailTemplates = { - verification: { - subject: 'Verify Your Shattered Void Account', - template: 'email-verification', - }, - passwordReset: { - subject: 'Reset Your Shattered Void Password', - template: 'password-reset', - }, - securityAlert: { - subject: 'Security Alert - Shattered Void', - template: 'security-alert', - }, - welcomeComplete: { - subject: 'Welcome to Shattered Void!', - template: 'welcome-complete', - }, - passwordChanged: { - subject: 'Password Changed - Shattered Void', - template: 'password-changed', - }, -}; - -/** - * Email sending configuration - */ -const sendingConfig = { - from: { - name: process.env.SMTP_FROM_NAME || 'Shattered Void', - address: process.env.SMTP_FROM || 'noreply@shatteredvoid.game', - }, - replyTo: { - name: process.env.SMTP_REPLY_NAME || 'Shattered Void Support', - address: process.env.SMTP_REPLY_TO || 'support@shatteredvoid.game', - }, - defaults: { - headers: { - 'X-Mailer': 'Shattered Void Game Server v1.0', - 'X-Priority': '3', - }, - }, - rateLimiting: { - maxPerHour: parseInt(process.env.EMAIL_RATE_LIMIT) || 100, - maxPerDay: parseInt(process.env.EMAIL_DAILY_LIMIT) || 1000, - }, -}; - -/** - * Development email configuration with additional debugging - */ -const developmentConfig = { - logEmails: true, - saveEmailsToFile: process.env.SAVE_DEV_EMAILS === 'true', - emailLogPath: process.env.EMAIL_LOG_PATH || './logs/emails.log', - mockDelay: parseInt(process.env.MOCK_EMAIL_DELAY) || 0, // Simulate network delay -}; - -/** - * Environment-specific email service factory - * @returns {Object} Email service configuration with methods - */ -function createEmailServiceConfig() { - const config = getEmailConfig(); - const validation = validateEmailConfig(config); - - if (!validation.isValid) { - logger.error('Invalid email configuration', { - errors: validation.errors, - }); - - if (process.env.NODE_ENV === 'production') { - throw new Error(`Email configuration validation failed: ${validation.errors.join(', ')}`); - } - } - - return { - ...config, - templates: emailTemplates, - sending: sendingConfig, - development: developmentConfig, - validation, - - /** - * Get template configuration - * @param {string} templateName - Template name - * @returns {Object} Template configuration - */ - getTemplate(templateName) { - const template = emailTemplates[templateName]; - if (!template) { - throw new Error(`Email template '${templateName}' not found`); - } - return template; - }, - - /** - * Get sender information - * @returns {Object} Sender configuration - */ - getSender() { - return { - from: `${sendingConfig.from.name} <${sendingConfig.from.address}>`, - replyTo: `${sendingConfig.replyTo.name} <${sendingConfig.replyTo.address}>`, - }; - }, - - /** - * Check if rate limiting allows sending - * @param {string} identifier - Rate limiting identifier (email/IP) - * @returns {Promise} Whether sending is allowed - */ - async checkRateLimit(identifier) { - // TODO: Implement rate limiting check with Redis - // For now, always allow - return true; - }, - }; -} - -module.exports = { - getEmailConfig, - validateEmailConfig, - createEmailServiceConfig, - emailTemplates, - sendingConfig, - developmentConfig, -}; \ No newline at end of file diff --git a/src/config/redis.js b/src/config/redis.js index 5ea4aec..bbfeb9e 100644 --- a/src/config/redis.js +++ b/src/config/redis.js @@ -8,15 +8,15 @@ const logger = require('../utils/logger'); // Configuration const REDIS_CONFIG = { - host: process.env.REDIS_HOST || 'localhost', - port: parseInt(process.env.REDIS_PORT) || 6379, - password: process.env.REDIS_PASSWORD || undefined, - db: parseInt(process.env.REDIS_DB) || 0, - retryDelayOnFailover: 100, - maxRetriesPerRequest: 3, - lazyConnect: true, - connectTimeout: 10000, - commandTimeout: 5000, + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT) || 6379, + password: process.env.REDIS_PASSWORD || undefined, + db: parseInt(process.env.REDIS_DB) || 0, + retryDelayOnFailover: 100, + maxRetriesPerRequest: 3, + lazyConnect: true, + connectTimeout: 10000, + commandTimeout: 5000, }; let client = null; @@ -27,59 +27,59 @@ let isConnected = false; * @returns {Object} Redis client instance */ function createRedisClient() { - const redisClient = redis.createClient({ - socket: { - host: REDIS_CONFIG.host, - port: REDIS_CONFIG.port, - connectTimeout: REDIS_CONFIG.connectTimeout, - commandTimeout: REDIS_CONFIG.commandTimeout, - reconnectStrategy: (retries) => { - if (retries > 10) { - logger.error('Redis reconnection failed after 10 attempts'); - return new Error('Redis reconnection failed'); - } - const delay = Math.min(retries * 50, 2000); - logger.warn(`Redis reconnecting in ${delay}ms (attempt ${retries})`); - return delay; - }, - }, - password: REDIS_CONFIG.password, - database: REDIS_CONFIG.db, - }); - - // Connection event handlers - redisClient.on('connect', () => { - logger.info('Redis client connected'); - }); - - redisClient.on('ready', () => { - isConnected = true; - logger.info('Redis client ready', { - host: REDIS_CONFIG.host, - port: REDIS_CONFIG.port, - database: REDIS_CONFIG.db, + const redisClient = redis.createClient({ + socket: { + host: REDIS_CONFIG.host, + port: REDIS_CONFIG.port, + connectTimeout: REDIS_CONFIG.connectTimeout, + commandTimeout: REDIS_CONFIG.commandTimeout, + reconnectStrategy: (retries) => { + if (retries > 10) { + logger.error('Redis reconnection failed after 10 attempts'); + return new Error('Redis reconnection failed'); + } + const delay = Math.min(retries * 50, 2000); + logger.warn(`Redis reconnecting in ${delay}ms (attempt ${retries})`); + return delay; + } + }, + password: REDIS_CONFIG.password, + database: REDIS_CONFIG.db, }); - }); - redisClient.on('error', (error) => { - isConnected = false; - logger.error('Redis client error:', { - message: error.message, - code: error.code, - stack: error.stack, + // Connection event handlers + redisClient.on('connect', () => { + logger.info('Redis client connected'); }); - }); - redisClient.on('end', () => { - isConnected = false; - logger.info('Redis client connection ended'); - }); + redisClient.on('ready', () => { + isConnected = true; + logger.info('Redis client ready', { + host: REDIS_CONFIG.host, + port: REDIS_CONFIG.port, + database: REDIS_CONFIG.db + }); + }); - redisClient.on('reconnecting', () => { - logger.info('Redis client reconnecting...'); - }); + redisClient.on('error', (error) => { + isConnected = false; + logger.error('Redis client error:', { + message: error.message, + code: error.code, + stack: error.stack + }); + }); - return redisClient; + redisClient.on('end', () => { + isConnected = false; + logger.info('Redis client connection ended'); + }); + + redisClient.on('reconnecting', () => { + logger.info('Redis client reconnecting...'); + }); + + return redisClient; } /** @@ -87,33 +87,33 @@ function createRedisClient() { * @returns {Promise} Redis client instance */ async function initializeRedis() { - try { - if (client && isConnected) { - logger.info('Redis already connected'); - return client; + try { + if (client && isConnected) { + logger.info('Redis already connected'); + return client; + } + + client = createRedisClient(); + await client.connect(); + + // Test connection + const pong = await client.ping(); + if (pong !== 'PONG') { + throw new Error('Redis ping test failed'); + } + + logger.info('Redis initialized successfully'); + return client; + + } catch (error) { + logger.error('Failed to initialize Redis:', { + host: REDIS_CONFIG.host, + port: REDIS_CONFIG.port, + error: error.message, + stack: error.stack + }); + throw error; } - - client = createRedisClient(); - await client.connect(); - - // Test connection - const pong = await client.ping(); - if (pong !== 'PONG') { - throw new Error('Redis ping test failed'); - } - - logger.info('Redis initialized successfully'); - return client; - - } catch (error) { - logger.error('Failed to initialize Redis:', { - host: REDIS_CONFIG.host, - port: REDIS_CONFIG.port, - error: error.message, - stack: error.stack, - }); - throw error; - } } /** @@ -121,11 +121,11 @@ async function initializeRedis() { * @returns {Object|null} Redis client or null if not connected */ function getRedisClient() { - if (!client || !isConnected) { - logger.warn('Redis client requested but not connected'); - return null; - } - return client; + if (!client || !isConnected) { + logger.warn('Redis client requested but not connected'); + return null; + } + return client; } /** @@ -133,7 +133,7 @@ function getRedisClient() { * @returns {boolean} Connection status */ function isRedisConnected() { - return isConnected && client !== null; + return isConnected && client !== null; } /** @@ -141,109 +141,109 @@ function isRedisConnected() { * @returns {Promise} */ async function closeRedis() { - try { - if (client && isConnected) { - await client.quit(); - client = null; - isConnected = false; - logger.info('Redis connection closed gracefully'); + try { + if (client && isConnected) { + await client.quit(); + client = null; + isConnected = false; + logger.info('Redis connection closed gracefully'); + } + } catch (error) { + logger.error('Error closing Redis connection:', error); + // Force close if graceful close fails + if (client) { + await client.disconnect(); + client = null; + isConnected = false; + } + throw error; } - } catch (error) { - logger.error('Error closing Redis connection:', error); - // Force close if graceful close fails - if (client) { - await client.disconnect(); - client = null; - isConnected = false; - } - throw error; - } } /** * Redis utility functions for common operations */ const RedisUtils = { - /** + /** * Set a key-value pair with optional expiration * @param {string} key - Redis key * @param {string} value - Value to store * @param {number} ttl - Time to live in seconds (optional) * @returns {Promise} Redis response */ - async set(key, value, ttl = null) { - const redisClient = getRedisClient(); - if (!redisClient) throw new Error('Redis not connected'); + async set(key, value, ttl = null) { + const redisClient = getRedisClient(); + if (!redisClient) throw new Error('Redis not connected'); - try { - if (ttl) { - return await redisClient.setEx(key, ttl, value); - } - return await redisClient.set(key, value); - } catch (error) { - logger.error('Redis SET error:', { key, error: error.message }); - throw error; - } - }, + try { + if (ttl) { + return await redisClient.setEx(key, ttl, value); + } + return await redisClient.set(key, value); + } catch (error) { + logger.error('Redis SET error:', { key, error: error.message }); + throw error; + } + }, - /** + /** * Get value by key * @param {string} key - Redis key * @returns {Promise} Value or null if not found */ - async get(key) { - const redisClient = getRedisClient(); - if (!redisClient) throw new Error('Redis not connected'); + async get(key) { + const redisClient = getRedisClient(); + if (!redisClient) throw new Error('Redis not connected'); - try { - return await redisClient.get(key); - } catch (error) { - logger.error('Redis GET error:', { key, error: error.message }); - throw error; - } - }, + try { + return await redisClient.get(key); + } catch (error) { + logger.error('Redis GET error:', { key, error: error.message }); + throw error; + } + }, - /** + /** * Delete a key * @param {string} key - Redis key * @returns {Promise} Number of keys deleted */ - async del(key) { - const redisClient = getRedisClient(); - if (!redisClient) throw new Error('Redis not connected'); + async del(key) { + const redisClient = getRedisClient(); + if (!redisClient) throw new Error('Redis not connected'); - try { - return await redisClient.del(key); - } catch (error) { - logger.error('Redis DEL error:', { key, error: error.message }); - throw error; - } - }, + try { + return await redisClient.del(key); + } catch (error) { + logger.error('Redis DEL error:', { key, error: error.message }); + throw error; + } + }, - /** + /** * Check if key exists * @param {string} key - Redis key * @returns {Promise} True if key exists */ - async exists(key) { - const redisClient = getRedisClient(); - if (!redisClient) throw new Error('Redis not connected'); + async exists(key) { + const redisClient = getRedisClient(); + if (!redisClient) throw new Error('Redis not connected'); - try { - const result = await redisClient.exists(key); - return result === 1; - } catch (error) { - logger.error('Redis EXISTS error:', { key, error: error.message }); - throw error; + try { + const result = await redisClient.exists(key); + return result === 1; + } catch (error) { + logger.error('Redis EXISTS error:', { key, error: error.message }); + throw error; + } } - }, }; module.exports = { - initializeRedis, - getRedisClient, - isRedisConnected, - closeRedis, - RedisUtils, - client: () => client, // For backward compatibility -}; + initializeRedis, + getRedisClient, + isRedisConnected, + closeRedis, + RedisUtils, + client: () => client // For backward compatibility +}; \ No newline at end of file diff --git a/src/config/websocket.js b/src/config/websocket.js index 124db98..931338e 100644 --- a/src/config/websocket.js +++ b/src/config/websocket.js @@ -8,18 +8,18 @@ const logger = require('../utils/logger'); // Configuration const WEBSOCKET_CONFIG = { - cors: { - origin: process.env.WEBSOCKET_CORS_ORIGIN?.split(',') || ['http://localhost:3000', 'http://localhost:3001'], - methods: ['GET', 'POST'], - credentials: true, - }, - pingTimeout: parseInt(process.env.WEBSOCKET_PING_TIMEOUT) || 20000, - pingInterval: parseInt(process.env.WEBSOCKET_PING_INTERVAL) || 25000, - maxHttpBufferSize: parseInt(process.env.WEBSOCKET_MAX_BUFFER_SIZE) || 1e6, // 1MB - transports: ['websocket', 'polling'], - allowEIO3: true, - compression: true, - httpCompression: true, + cors: { + origin: process.env.WEBSOCKET_CORS_ORIGIN?.split(',') || ['http://localhost:3000', 'http://localhost:3001'], + methods: ['GET', 'POST'], + credentials: true + }, + pingTimeout: parseInt(process.env.WEBSOCKET_PING_TIMEOUT) || 20000, + pingInterval: parseInt(process.env.WEBSOCKET_PING_INTERVAL) || 25000, + maxHttpBufferSize: parseInt(process.env.WEBSOCKET_MAX_BUFFER_SIZE) || 1e6, // 1MB + transports: ['websocket', 'polling'], + allowEIO3: true, + compression: true, + httpCompression: true }; let io = null; @@ -32,99 +32,99 @@ const connectedClients = new Map(); * @returns {Promise} Socket.IO server instance */ async function initializeWebSocket(server) { - try { - if (io) { - logger.info('WebSocket server already initialized'); - return io; + try { + if (io) { + logger.info('WebSocket server already initialized'); + return io; + } + + // Create Socket.IO server + io = new Server(server, WEBSOCKET_CONFIG); + + // Set up middleware for authentication and logging + io.use(async (socket, next) => { + const correlationId = socket.handshake.query.correlationId || require('uuid').v4(); + socket.correlationId = correlationId; + + logger.info('WebSocket connection attempt', { + correlationId, + socketId: socket.id, + ip: socket.handshake.address, + userAgent: socket.handshake.headers['user-agent'] + }); + + next(); + }); + + // Connection event handler + io.on('connection', (socket) => { + connectionCount++; + connectedClients.set(socket.id, { + connectedAt: new Date(), + ip: socket.handshake.address, + userAgent: socket.handshake.headers['user-agent'], + playerId: null, // Will be set after authentication + rooms: new Set() + }); + + logger.info('WebSocket client connected', { + correlationId: socket.correlationId, + socketId: socket.id, + totalConnections: connectionCount, + ip: socket.handshake.address + }); + + // Set up event handlers + setupSocketEventHandlers(socket); + + // Handle disconnection + socket.on('disconnect', (reason) => { + connectionCount--; + const clientInfo = connectedClients.get(socket.id); + connectedClients.delete(socket.id); + + logger.info('WebSocket client disconnected', { + correlationId: socket.correlationId, + socketId: socket.id, + reason, + totalConnections: connectionCount, + playerId: clientInfo?.playerId, + connectionDuration: clientInfo ? Date.now() - clientInfo.connectedAt : 0 + }); + }); + + // Handle connection errors + socket.on('error', (error) => { + logger.error('WebSocket connection error', { + correlationId: socket.correlationId, + socketId: socket.id, + error: error.message, + stack: error.stack + }); + }); + }); + + // Server-level error handling + io.engine.on('connection_error', (error) => { + logger.error('WebSocket connection error:', { + message: error.message, + code: error.code, + context: error.context + }); + }); + + logger.info('WebSocket server initialized successfully', { + maxConnections: process.env.WEBSOCKET_MAX_CONNECTIONS || 'unlimited', + pingTimeout: WEBSOCKET_CONFIG.pingTimeout, + pingInterval: WEBSOCKET_CONFIG.pingInterval + }); + + return io; + + } catch (error) { + logger.error('Failed to initialize WebSocket server:', error); + throw error; } - - // Create Socket.IO server - io = new Server(server, WEBSOCKET_CONFIG); - - // Set up middleware for authentication and logging - io.use(async (socket, next) => { - const correlationId = socket.handshake.query.correlationId || require('uuid').v4(); - socket.correlationId = correlationId; - - logger.info('WebSocket connection attempt', { - correlationId, - socketId: socket.id, - ip: socket.handshake.address, - userAgent: socket.handshake.headers['user-agent'], - }); - - next(); - }); - - // Connection event handler - io.on('connection', (socket) => { - connectionCount++; - connectedClients.set(socket.id, { - connectedAt: new Date(), - ip: socket.handshake.address, - userAgent: socket.handshake.headers['user-agent'], - playerId: null, // Will be set after authentication - rooms: new Set(), - }); - - logger.info('WebSocket client connected', { - correlationId: socket.correlationId, - socketId: socket.id, - totalConnections: connectionCount, - ip: socket.handshake.address, - }); - - // Set up event handlers - setupSocketEventHandlers(socket); - - // Handle disconnection - socket.on('disconnect', (reason) => { - connectionCount--; - const clientInfo = connectedClients.get(socket.id); - connectedClients.delete(socket.id); - - logger.info('WebSocket client disconnected', { - correlationId: socket.correlationId, - socketId: socket.id, - reason, - totalConnections: connectionCount, - playerId: clientInfo?.playerId, - connectionDuration: clientInfo ? Date.now() - clientInfo.connectedAt : 0, - }); - }); - - // Handle connection errors - socket.on('error', (error) => { - logger.error('WebSocket connection error', { - correlationId: socket.correlationId, - socketId: socket.id, - error: error.message, - stack: error.stack, - }); - }); - }); - - // Server-level error handling - io.engine.on('connection_error', (error) => { - logger.error('WebSocket connection error:', { - message: error.message, - code: error.code, - context: error.context, - }); - }); - - logger.info('WebSocket server initialized successfully', { - maxConnections: process.env.WEBSOCKET_MAX_CONNECTIONS || 'unlimited', - pingTimeout: WEBSOCKET_CONFIG.pingTimeout, - pingInterval: WEBSOCKET_CONFIG.pingInterval, - }); - - return io; - - } catch (error) { - logger.error('Failed to initialize WebSocket server:', error); - throw error; - } } /** @@ -132,97 +132,97 @@ async function initializeWebSocket(server) { * @param {Object} socket - Socket.IO socket instance */ function setupSocketEventHandlers(socket) { - // Player authentication - socket.on('authenticate', async (data) => { - try { - logger.info('WebSocket authentication attempt', { - correlationId: socket.correlationId, - socketId: socket.id, - playerId: data?.playerId, - }); + // Player authentication + socket.on('authenticate', async (data) => { + try { + logger.info('WebSocket authentication attempt', { + correlationId: socket.correlationId, + socketId: socket.id, + playerId: data?.playerId + }); - // TODO: Implement JWT token validation - // For now, just acknowledge - socket.emit('authenticated', { - success: true, - message: 'Authentication successful', - }); + // TODO: Implement JWT token validation + // For now, just acknowledge + socket.emit('authenticated', { + success: true, + message: 'Authentication successful' + }); - // Update client information - if (connectedClients.has(socket.id)) { - connectedClients.get(socket.id).playerId = data?.playerId; - } + // Update client information + if (connectedClients.has(socket.id)) { + connectedClients.get(socket.id).playerId = data?.playerId; + } - } catch (error) { - logger.error('WebSocket authentication error', { - correlationId: socket.correlationId, - socketId: socket.id, - error: error.message, - }); + } catch (error) { + logger.error('WebSocket authentication error', { + correlationId: socket.correlationId, + socketId: socket.id, + error: error.message + }); - socket.emit('authentication_error', { - success: false, - message: 'Authentication failed', - }); - } - }); - - // Join room (for game features like galaxy regions, player groups, etc.) - socket.on('join_room', (roomName) => { - if (typeof roomName !== 'string' || roomName.length > 50) { - socket.emit('error', { message: 'Invalid room name' }); - return; - } - - socket.join(roomName); - - const clientInfo = connectedClients.get(socket.id); - if (clientInfo) { - clientInfo.rooms.add(roomName); - } - - logger.info('Client joined room', { - correlationId: socket.correlationId, - socketId: socket.id, - room: roomName, - playerId: clientInfo?.playerId, + socket.emit('authentication_error', { + success: false, + message: 'Authentication failed' + }); + } }); - socket.emit('room_joined', { room: roomName }); - }); + // Join room (for game features like galaxy regions, player groups, etc.) + socket.on('join_room', (roomName) => { + if (typeof roomName !== 'string' || roomName.length > 50) { + socket.emit('error', { message: 'Invalid room name' }); + return; + } - // Leave room - socket.on('leave_room', (roomName) => { - socket.leave(roomName); + socket.join(roomName); + + const clientInfo = connectedClients.get(socket.id); + if (clientInfo) { + clientInfo.rooms.add(roomName); + } - const clientInfo = connectedClients.get(socket.id); - if (clientInfo) { - clientInfo.rooms.delete(roomName); - } + logger.info('Client joined room', { + correlationId: socket.correlationId, + socketId: socket.id, + room: roomName, + playerId: clientInfo?.playerId + }); - logger.info('Client left room', { - correlationId: socket.correlationId, - socketId: socket.id, - room: roomName, - playerId: clientInfo?.playerId, + socket.emit('room_joined', { room: roomName }); }); - socket.emit('room_left', { room: roomName }); - }); + // Leave room + socket.on('leave_room', (roomName) => { + socket.leave(roomName); + + const clientInfo = connectedClients.get(socket.id); + if (clientInfo) { + clientInfo.rooms.delete(roomName); + } - // Ping/pong for connection testing - socket.on('ping', () => { - socket.emit('pong', { timestamp: Date.now() }); - }); + logger.info('Client left room', { + correlationId: socket.correlationId, + socketId: socket.id, + room: roomName, + playerId: clientInfo?.playerId + }); - // Generic message handler (for debugging) - socket.on('message', (data) => { - logger.debug('WebSocket message received', { - correlationId: socket.correlationId, - socketId: socket.id, - data: typeof data === 'object' ? JSON.stringify(data) : data, + socket.emit('room_left', { room: roomName }); + }); + + // Ping/pong for connection testing + socket.on('ping', () => { + socket.emit('pong', { timestamp: Date.now() }); + }); + + // Generic message handler (for debugging) + socket.on('message', (data) => { + logger.debug('WebSocket message received', { + correlationId: socket.correlationId, + socketId: socket.id, + data: typeof data === 'object' ? JSON.stringify(data) : data + }); }); - }); } /** @@ -230,7 +230,7 @@ function setupSocketEventHandlers(socket) { * @returns {Object|null} Socket.IO server instance */ function getWebSocketServer() { - return io; + return io; } /** @@ -238,14 +238,14 @@ function getWebSocketServer() { * @returns {Object} Connection statistics */ function getConnectionStats() { - return { - totalConnections: connectionCount, - authenticatedConnections: Array.from(connectedClients.values()) - .filter(client => client.playerId).length, - anonymousConnections: Array.from(connectedClients.values()) - .filter(client => !client.playerId).length, - rooms: io ? Array.from(io.sockets.adapter.rooms.keys()) : [], - }; + return { + totalConnections: connectionCount, + authenticatedConnections: Array.from(connectedClients.values()) + .filter(client => client.playerId).length, + anonymousConnections: Array.from(connectedClients.values()) + .filter(client => !client.playerId).length, + rooms: io ? Array.from(io.sockets.adapter.rooms.keys()) : [] + }; } /** @@ -254,16 +254,16 @@ function getConnectionStats() { * @param {Object} data - Data to broadcast */ function broadcastToAll(event, data) { - if (!io) { - logger.warn('Attempted to broadcast but WebSocket server not initialized'); - return; - } + if (!io) { + logger.warn('Attempted to broadcast but WebSocket server not initialized'); + return; + } - io.emit(event, data); - logger.info('Broadcast sent to all clients', { - event, - recipientCount: connectionCount, - }); + io.emit(event, data); + logger.info('Broadcast sent to all clients', { + event, + recipientCount: connectionCount + }); } /** @@ -273,17 +273,17 @@ function broadcastToAll(event, data) { * @param {Object} data - Data to broadcast */ function broadcastToRoom(room, event, data) { - if (!io) { - logger.warn('Attempted to broadcast to room but WebSocket server not initialized'); - return; - } + if (!io) { + logger.warn('Attempted to broadcast to room but WebSocket server not initialized'); + return; + } - io.to(room).emit(event, data); - logger.info('Broadcast sent to room', { - room, - event, - recipientCount: io.sockets.adapter.rooms.get(room)?.size || 0, - }); + io.to(room).emit(event, data); + logger.info('Broadcast sent to room', { + room, + event, + recipientCount: io.sockets.adapter.rooms.get(room)?.size || 0 + }); } /** @@ -291,31 +291,31 @@ function broadcastToRoom(room, event, data) { * @returns {Promise} */ async function closeWebSocket() { - if (!io) return; + if (!io) return; - try { - // Disconnect all clients - io.disconnectSockets(); + try { + // Disconnect all clients + io.disconnectSockets(); + + // Close server + io.close(); + + io = null; + connectionCount = 0; + connectedClients.clear(); - // Close server - io.close(); - - io = null; - connectionCount = 0; - connectedClients.clear(); - - logger.info('WebSocket server closed gracefully'); - } catch (error) { - logger.error('Error closing WebSocket server:', error); - throw error; - } + logger.info('WebSocket server closed gracefully'); + } catch (error) { + logger.error('Error closing WebSocket server:', error); + throw error; + } } module.exports = { - initializeWebSocket, - getWebSocketServer, - getConnectionStats, - broadcastToAll, - broadcastToRoom, - closeWebSocket, -}; + initializeWebSocket, + getWebSocketServer, + getConnectionStats, + broadcastToAll, + broadcastToRoom, + closeWebSocket +}; \ No newline at end of file diff --git a/src/controllers/admin/auth.controller.js b/src/controllers/admin/auth.controller.js index 23473bc..cf9f913 100644 --- a/src/controllers/admin/auth.controller.js +++ b/src/controllers/admin/auth.controller.js @@ -14,45 +14,45 @@ const adminService = new AdminService(); * POST /api/admin/auth/login */ const login = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { email, password } = req.body; + const correlationId = req.correlationId; + const { email, password } = req.body; - logger.info('Admin login request received', { - correlationId, - email, - }); + logger.info('Admin login request received', { + correlationId, + email + }); - const authResult = await adminService.authenticateAdmin({ - email, - password, - }, correlationId); + const authResult = await adminService.authenticateAdmin({ + email, + password + }, correlationId); - logger.audit('Admin login successful', { - correlationId, - adminId: authResult.admin.id, - email: authResult.admin.email, - username: authResult.admin.username, - permissions: authResult.admin.permissions, - }); + logger.audit('Admin login successful', { + correlationId, + adminId: authResult.admin.id, + email: authResult.admin.email, + username: authResult.admin.username, + permissions: authResult.admin.permissions + }); - // Set refresh token as httpOnly cookie - res.cookie('adminRefreshToken', authResult.tokens.refreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: 8 * 60 * 60 * 1000, // 8 hours (shorter than player tokens) - path: '/api/admin', // Restrict to admin routes - }); + // Set refresh token as httpOnly cookie + res.cookie('adminRefreshToken', authResult.tokens.refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 8 * 60 * 60 * 1000, // 8 hours (shorter than player tokens) + path: '/api/admin' // Restrict to admin routes + }); - res.status(200).json({ - success: true, - message: 'Admin login successful', - data: { - admin: authResult.admin, - accessToken: authResult.tokens.accessToken, - }, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Admin login successful', + data: { + admin: authResult.admin, + accessToken: authResult.tokens.accessToken + }, + correlationId + }); }); /** @@ -60,31 +60,31 @@ const login = asyncHandler(async (req, res) => { * POST /api/admin/auth/logout */ const logout = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const adminId = req.user?.adminId; + const correlationId = req.correlationId; + const adminId = req.user?.adminId; - logger.audit('Admin logout request received', { - correlationId, - adminId, - }); + logger.audit('Admin logout request received', { + correlationId, + adminId + }); - // Clear refresh token cookie - res.clearCookie('adminRefreshToken', { - path: '/api/admin', - }); + // Clear refresh token cookie + res.clearCookie('adminRefreshToken', { + path: '/api/admin' + }); - // TODO: Add token to blacklist if implementing token blacklisting + // TODO: Add token to blacklist if implementing token blacklisting - logger.audit('Admin logout successful', { - correlationId, - adminId, - }); + logger.audit('Admin logout successful', { + correlationId, + adminId + }); - res.status(200).json({ - success: true, - message: 'Admin logout successful', - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Admin logout successful', + correlationId + }); }); /** @@ -92,30 +92,30 @@ const logout = asyncHandler(async (req, res) => { * GET /api/admin/auth/me */ const getProfile = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const adminId = req.user.adminId; + const correlationId = req.correlationId; + const adminId = req.user.adminId; - logger.info('Admin profile request received', { - correlationId, - adminId, - }); + logger.info('Admin profile request received', { + correlationId, + adminId + }); - const profile = await adminService.getAdminProfile(adminId, correlationId); + const profile = await adminService.getAdminProfile(adminId, correlationId); - logger.info('Admin profile retrieved', { - correlationId, - adminId, - username: profile.username, - }); + logger.info('Admin profile retrieved', { + correlationId, + adminId, + username: profile.username + }); - res.status(200).json({ - success: true, - message: 'Admin profile retrieved successfully', - data: { - admin: profile, - }, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Admin profile retrieved successfully', + data: { + admin: profile + }, + correlationId + }); }); /** @@ -123,32 +123,32 @@ const getProfile = asyncHandler(async (req, res) => { * GET /api/admin/auth/verify */ const verifyToken = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const user = req.user; + const correlationId = req.correlationId; + const user = req.user; - logger.audit('Admin token verification request received', { - correlationId, - adminId: user.adminId, - username: user.username, - permissions: user.permissions, - }); - - res.status(200).json({ - success: true, - message: 'Admin token is valid', - data: { - admin: { + logger.audit('Admin token verification request received', { + correlationId, adminId: user.adminId, - email: user.email, username: user.username, - permissions: user.permissions, - type: user.type, - tokenIssuedAt: new Date(user.iat * 1000), - tokenExpiresAt: new Date(user.exp * 1000), - }, - }, - correlationId, - }); + permissions: user.permissions + }); + + res.status(200).json({ + success: true, + message: 'Admin token is valid', + data: { + admin: { + adminId: user.adminId, + email: user.email, + username: user.username, + permissions: user.permissions, + type: user.type, + tokenIssuedAt: new Date(user.iat * 1000), + tokenExpiresAt: new Date(user.exp * 1000) + } + }, + correlationId + }); }); /** @@ -156,31 +156,31 @@ const verifyToken = asyncHandler(async (req, res) => { * POST /api/admin/auth/refresh */ const refresh = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const refreshToken = req.cookies.adminRefreshToken; + const correlationId = req.correlationId; + const refreshToken = req.cookies.adminRefreshToken; - if (!refreshToken) { - logger.warn('Admin token refresh request without refresh token', { - correlationId, + if (!refreshToken) { + logger.warn('Admin token refresh request without refresh token', { + correlationId + }); + + return res.status(401).json({ + success: false, + message: 'Admin refresh token not provided', + correlationId + }); + } + + // TODO: Implement admin refresh token validation and new token generation + logger.warn('Admin token refresh requested but not implemented', { + correlationId }); - return res.status(401).json({ - success: false, - message: 'Admin refresh token not provided', - correlationId, + res.status(501).json({ + success: false, + message: 'Admin token refresh feature not yet implemented', + correlationId }); - } - - // TODO: Implement admin refresh token validation and new token generation - logger.warn('Admin token refresh requested but not implemented', { - correlationId, - }); - - res.status(501).json({ - success: false, - message: 'Admin token refresh feature not yet implemented', - correlationId, - }); }); /** @@ -188,31 +188,31 @@ const refresh = asyncHandler(async (req, res) => { * GET /api/admin/auth/stats */ const getSystemStats = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const adminId = req.user.adminId; + const correlationId = req.correlationId; + const adminId = req.user.adminId; - logger.audit('System statistics request received', { - correlationId, - adminId, - }); + logger.audit('System statistics request received', { + correlationId, + adminId + }); - const stats = await adminService.getSystemStats(correlationId); + const stats = await adminService.getSystemStats(correlationId); - logger.audit('System statistics retrieved', { - correlationId, - adminId, - totalPlayers: stats.players.total, - activePlayers: stats.players.active, - }); + logger.audit('System statistics retrieved', { + correlationId, + adminId, + totalPlayers: stats.players.total, + activePlayers: stats.players.active + }); - res.status(200).json({ - success: true, - message: 'System statistics retrieved successfully', - data: { - stats, - }, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'System statistics retrieved successfully', + data: { + stats + }, + correlationId + }); }); /** @@ -220,42 +220,42 @@ const getSystemStats = asyncHandler(async (req, res) => { * POST /api/admin/auth/change-password */ const changePassword = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const adminId = req.user.adminId; - const { currentPassword, newPassword } = req.body; + const correlationId = req.correlationId; + const adminId = req.user.adminId; + const { currentPassword, newPassword } = req.body; - logger.audit('Admin password change request received', { - correlationId, - adminId, - }); + logger.audit('Admin password change request received', { + correlationId, + adminId + }); - // TODO: Implement admin password change functionality - // This would involve: - // 1. Verify current password - // 2. Validate new password strength - // 3. Hash new password - // 4. Update in database - // 5. Optionally invalidate existing tokens - // 6. Send notification email + // TODO: Implement admin password change functionality + // This would involve: + // 1. Verify current password + // 2. Validate new password strength + // 3. Hash new password + // 4. Update in database + // 5. Optionally invalidate existing tokens + // 6. Send notification email - logger.warn('Admin password change requested but not implemented', { - correlationId, - adminId, - }); + logger.warn('Admin password change requested but not implemented', { + correlationId, + adminId + }); - res.status(501).json({ - success: false, - message: 'Admin password change feature not yet implemented', - correlationId, - }); + res.status(501).json({ + success: false, + message: 'Admin password change feature not yet implemented', + correlationId + }); }); module.exports = { - login, - logout, - getProfile, - verifyToken, - refresh, - getSystemStats, - changePassword, -}; + login, + logout, + getProfile, + verifyToken, + refresh, + getSystemStats, + changePassword +}; \ No newline at end of file diff --git a/src/controllers/admin/combat.controller.js b/src/controllers/admin/combat.controller.js deleted file mode 100644 index 51fc64c..0000000 --- a/src/controllers/admin/combat.controller.js +++ /dev/null @@ -1,739 +0,0 @@ -/** - * Admin Combat Controller - * Handles administrative combat management operations - */ - -const CombatService = require('../../services/combat/CombatService'); -const { CombatPluginManager } = require('../../services/combat/CombatPluginManager'); -const GameEventService = require('../../services/websocket/GameEventService'); -const db = require('../../database/connection'); -const logger = require('../../utils/logger'); -const { ValidationError, ConflictError, NotFoundError } = require('../../middleware/error.middleware'); - -class AdminCombatController { - constructor() { - this.combatPluginManager = null; - this.gameEventService = null; - this.combatService = null; - } - - /** - * Initialize controller with dependencies - */ - async initialize(dependencies = {}) { - this.gameEventService = dependencies.gameEventService || new GameEventService(); - this.combatPluginManager = dependencies.combatPluginManager || new CombatPluginManager(); - this.combatService = dependencies.combatService || new CombatService(this.gameEventService, this.combatPluginManager); - - await this.combatPluginManager.initialize('admin-controller-init'); - } - - /** - * Get combat system statistics - * GET /api/admin/combat/statistics - */ - async getCombatStatistics(req, res, next) { - try { - const correlationId = req.correlationId; - - logger.info('Admin combat statistics request', { - correlationId, - adminUser: req.user.id, - }); - - if (!this.combatService) { - await this.initialize(); - } - - // Get overall combat statistics - const [ - totalBattles, - activeBattles, - completedToday, - averageDuration, - queueStatus, - playerStats, - ] = await Promise.all([ - // Total battles - db('battles').count('* as count').first(), - - // Active battles - db('battles').where('status', 'active').count('* as count').first(), - - // Battles completed today - db('battles') - .where('status', 'completed') - .where('completed_at', '>=', new Date(Date.now() - 24 * 60 * 60 * 1000)) - .count('* as count') - .first(), - - // Average battle duration - db('combat_encounters') - .avg('duration_seconds as avg_duration') - .first(), - - // Combat queue status - db('combat_queue') - .select('queue_status') - .count('* as count') - .groupBy('queue_status'), - - // Top player statistics - db('combat_statistics') - .select([ - 'player_id', - 'battles_won', - 'battles_lost', - 'ships_destroyed', - 'total_experience_gained', - ]) - .orderBy('battles_won', 'desc') - .limit(10), - ]); - - // Combat outcome distribution - const outcomeStats = await db('combat_encounters') - .select('outcome') - .count('* as count') - .groupBy('outcome'); - - // Battle type distribution - const typeStats = await db('battles') - .select('battle_type') - .count('* as count') - .groupBy('battle_type'); - - const statistics = { - overall: { - total_battles: parseInt(totalBattles.count), - active_battles: parseInt(activeBattles.count), - completed_today: parseInt(completedToday.count), - average_duration_seconds: parseFloat(averageDuration.avg_duration) || 0, - }, - queue: queueStatus.reduce((acc, status) => { - acc[status.queue_status] = parseInt(status.count); - return acc; - }, {}), - outcomes: outcomeStats.reduce((acc, outcome) => { - acc[outcome.outcome] = parseInt(outcome.count); - return acc; - }, {}), - battle_types: typeStats.reduce((acc, type) => { - acc[type.battle_type] = parseInt(type.count); - return acc; - }, {}), - top_players: playerStats, - }; - - logger.info('Combat statistics retrieved', { - correlationId, - adminUser: req.user.id, - totalBattles: statistics.overall.total_battles, - }); - - res.json({ - success: true, - data: statistics, - }); - - } catch (error) { - logger.error('Failed to get combat statistics', { - correlationId: req.correlationId, - adminUser: req.user?.id, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } - - /** - * Get combat queue with detailed information - * GET /api/admin/combat/queue - */ - async getCombatQueue(req, res, next) { - try { - const correlationId = req.correlationId; - const { status, limit = 50, priority_min, priority_max } = req.query; - - logger.info('Admin combat queue request', { - correlationId, - adminUser: req.user.id, - status, - limit, - }); - - if (!this.combatService) { - await this.initialize(); - } - - let query = db('combat_queue') - .select([ - 'combat_queue.*', - 'battles.battle_type', - 'battles.location', - 'battles.status as battle_status', - 'battles.participants', - 'battles.estimated_duration', - ]) - .join('battles', 'combat_queue.battle_id', 'battles.id') - .orderBy('combat_queue.priority', 'desc') - .orderBy('combat_queue.scheduled_at', 'asc') - .limit(parseInt(limit)); - - if (status) { - query = query.where('combat_queue.queue_status', status); - } - - if (priority_min) { - query = query.where('combat_queue.priority', '>=', parseInt(priority_min)); - } - - if (priority_max) { - query = query.where('combat_queue.priority', '<=', parseInt(priority_max)); - } - - const queue = await query; - - // Get queue summary - const queueSummary = await db('combat_queue') - .select('queue_status') - .count('* as count') - .groupBy('queue_status'); - - const result = { - queue: queue.map(item => ({ - ...item, - participants: JSON.parse(item.participants), - processing_metadata: item.processing_metadata ? JSON.parse(item.processing_metadata) : null, - })), - summary: queueSummary.reduce((acc, item) => { - acc[item.queue_status] = parseInt(item.count); - return acc; - }, {}), - total_in_query: queue.length, - }; - - logger.info('Combat queue retrieved', { - correlationId, - adminUser: req.user.id, - queueSize: queue.length, - }); - - res.json({ - success: true, - data: result, - }); - - } catch (error) { - logger.error('Failed to get combat queue', { - correlationId: req.correlationId, - adminUser: req.user?.id, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } - - /** - * Force resolve a combat - * POST /api/admin/combat/resolve/:battleId - */ - async forceResolveCombat(req, res, next) { - try { - const correlationId = req.correlationId; - const battleId = parseInt(req.params.battleId); - - logger.info('Admin force resolve combat request', { - correlationId, - adminUser: req.user.id, - battleId, - }); - - if (!this.combatService) { - await this.initialize(); - } - - const result = await this.combatService.processCombat(battleId, correlationId); - - // Log admin action - await db('audit_log').insert({ - entity_type: 'battle', - entity_id: battleId, - action: 'force_resolve_combat', - actor_type: 'admin', - actor_id: req.user.id, - changes: JSON.stringify({ - outcome: result.outcome, - duration: result.duration, - }), - metadata: JSON.stringify({ - correlation_id: correlationId, - admin_forced: true, - }), - ip_address: req.ip, - user_agent: req.get('User-Agent'), - }); - - logger.info('Combat force resolved by admin', { - correlationId, - adminUser: req.user.id, - battleId, - outcome: result.outcome, - }); - - res.json({ - success: true, - data: result, - message: 'Combat resolved successfully', - }); - - } catch (error) { - logger.error('Failed to force resolve combat', { - correlationId: req.correlationId, - adminUser: req.user?.id, - battleId: req.params.battleId, - error: error.message, - stack: error.stack, - }); - - if (error instanceof NotFoundError) { - return res.status(404).json({ - error: error.message, - code: 'BATTLE_NOT_FOUND', - }); - } - - if (error instanceof ConflictError) { - return res.status(409).json({ - error: error.message, - code: 'BATTLE_CONFLICT', - }); - } - - next(error); - } - } - - /** - * Cancel a battle - * POST /api/admin/combat/cancel/:battleId - */ - async cancelBattle(req, res, next) { - try { - const correlationId = req.correlationId; - const battleId = parseInt(req.params.battleId); - const { reason } = req.body; - - logger.info('Admin cancel battle request', { - correlationId, - adminUser: req.user.id, - battleId, - reason, - }); - - // Get battle details - const battle = await db('battles').where('id', battleId).first(); - if (!battle) { - return res.status(404).json({ - error: 'Battle not found', - code: 'BATTLE_NOT_FOUND', - }); - } - - if (battle.status === 'completed' || battle.status === 'cancelled') { - return res.status(409).json({ - error: 'Battle is already completed or cancelled', - code: 'BATTLE_ALREADY_FINISHED', - }); - } - - // Cancel the battle - await db.transaction(async (trx) => { - // Update battle status - await trx('battles') - .where('id', battleId) - .update({ - status: 'cancelled', - result: JSON.stringify({ - outcome: 'cancelled', - reason: reason || 'Cancelled by administrator', - cancelled_by: req.user.id, - cancelled_at: new Date(), - }), - completed_at: new Date(), - }); - - // Update combat queue - await trx('combat_queue') - .where('battle_id', battleId) - .update({ - queue_status: 'failed', - error_message: `Cancelled by administrator: ${reason || 'No reason provided'}`, - completed_at: new Date(), - }); - - // Reset fleet statuses - const participants = JSON.parse(battle.participants); - if (participants.attacker_fleet_id) { - await trx('fleets') - .where('id', participants.attacker_fleet_id) - .update({ - fleet_status: 'idle', - last_updated: new Date(), - }); - } - - if (participants.defender_fleet_id) { - await trx('fleets') - .where('id', participants.defender_fleet_id) - .update({ - fleet_status: 'idle', - last_updated: new Date(), - }); - } - - // Reset colony siege status - if (participants.defender_colony_id) { - await trx('colonies') - .where('id', participants.defender_colony_id) - .update({ - under_siege: false, - last_updated: new Date(), - }); - } - - // Log admin action - await trx('audit_log').insert({ - entity_type: 'battle', - entity_id: battleId, - action: 'cancel_battle', - actor_type: 'admin', - actor_id: req.user.id, - changes: JSON.stringify({ - old_status: battle.status, - new_status: 'cancelled', - reason, - }), - metadata: JSON.stringify({ - correlation_id: correlationId, - participants, - }), - ip_address: req.ip, - user_agent: req.get('User-Agent'), - }); - }); - - // Emit WebSocket event - if (this.gameEventService) { - this.gameEventService.emitCombatStatusUpdate(battleId, 'cancelled', { - reason: reason || 'Cancelled by administrator', - cancelled_by: req.user.id, - }, correlationId); - } - - logger.info('Battle cancelled by admin', { - correlationId, - adminUser: req.user.id, - battleId, - reason, - }); - - res.json({ - success: true, - message: 'Battle cancelled successfully', - }); - - } catch (error) { - logger.error('Failed to cancel battle', { - correlationId: req.correlationId, - adminUser: req.user?.id, - battleId: req.params.battleId, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } - - /** - * Get combat configurations - * GET /api/admin/combat/configurations - */ - async getCombatConfigurations(req, res, next) { - try { - const correlationId = req.correlationId; - - logger.info('Admin combat configurations request', { - correlationId, - adminUser: req.user.id, - }); - - const configurations = await db('combat_configurations') - .orderBy('combat_type') - .orderBy('config_name'); - - logger.info('Combat configurations retrieved', { - correlationId, - adminUser: req.user.id, - count: configurations.length, - }); - - res.json({ - success: true, - data: configurations, - }); - - } catch (error) { - logger.error('Failed to get combat configurations', { - correlationId: req.correlationId, - adminUser: req.user?.id, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } - - /** - * Create or update combat configuration - * POST /api/admin/combat/configurations - * PUT /api/admin/combat/configurations/:configId - */ - async saveCombatConfiguration(req, res, next) { - try { - const correlationId = req.correlationId; - const configId = req.params.configId ? parseInt(req.params.configId) : null; - const configData = req.body; - - logger.info('Admin save combat configuration request', { - correlationId, - adminUser: req.user.id, - configId, - isUpdate: !!configId, - }); - - const result = await db.transaction(async (trx) => { - let savedConfig; - - if (configId) { - // Update existing configuration - const existingConfig = await trx('combat_configurations') - .where('id', configId) - .first(); - - if (!existingConfig) { - throw new NotFoundError('Combat configuration not found'); - } - - await trx('combat_configurations') - .where('id', configId) - .update({ - ...configData, - updated_at: new Date(), - }); - - savedConfig = await trx('combat_configurations') - .where('id', configId) - .first(); - - // Log admin action - await trx('audit_log').insert({ - entity_type: 'combat_configuration', - entity_id: configId, - action: 'update_combat_configuration', - actor_type: 'admin', - actor_id: req.user.id, - changes: JSON.stringify({ - old_config: existingConfig, - new_config: savedConfig, - }), - metadata: JSON.stringify({ - correlation_id: correlationId, - }), - ip_address: req.ip, - user_agent: req.get('User-Agent'), - }); - - } else { - // Create new configuration - const [newConfig] = await trx('combat_configurations') - .insert({ - ...configData, - created_at: new Date(), - updated_at: new Date(), - }) - .returning('*'); - - savedConfig = newConfig; - - // Log admin action - await trx('audit_log').insert({ - entity_type: 'combat_configuration', - entity_id: savedConfig.id, - action: 'create_combat_configuration', - actor_type: 'admin', - actor_id: req.user.id, - changes: JSON.stringify({ - new_config: savedConfig, - }), - metadata: JSON.stringify({ - correlation_id: correlationId, - }), - ip_address: req.ip, - user_agent: req.get('User-Agent'), - }); - } - - return savedConfig; - }); - - logger.info('Combat configuration saved', { - correlationId, - adminUser: req.user.id, - configId: result.id, - configName: result.config_name, - }); - - res.status(configId ? 200 : 201).json({ - success: true, - data: result, - message: `Combat configuration ${configId ? 'updated' : 'created'} successfully`, - }); - - } catch (error) { - logger.error('Failed to save combat configuration', { - correlationId: req.correlationId, - adminUser: req.user?.id, - configId: req.params.configId, - error: error.message, - stack: error.stack, - }); - - if (error instanceof NotFoundError) { - return res.status(404).json({ - error: error.message, - code: 'CONFIG_NOT_FOUND', - }); - } - - if (error instanceof ValidationError) { - return res.status(400).json({ - error: error.message, - code: 'VALIDATION_ERROR', - }); - } - - next(error); - } - } - - /** - * Delete combat configuration - * DELETE /api/admin/combat/configurations/:configId - */ - async deleteCombatConfiguration(req, res, next) { - try { - const correlationId = req.correlationId; - const configId = parseInt(req.params.configId); - - logger.info('Admin delete combat configuration request', { - correlationId, - adminUser: req.user.id, - configId, - }); - - const config = await db('combat_configurations') - .where('id', configId) - .first(); - - if (!config) { - return res.status(404).json({ - error: 'Combat configuration not found', - code: 'CONFIG_NOT_FOUND', - }); - } - - // Check if configuration is in use - const inUse = await db('battles') - .where('combat_configuration_id', configId) - .where('status', 'active') - .first(); - - if (inUse) { - return res.status(409).json({ - error: 'Cannot delete configuration that is currently in use', - code: 'CONFIG_IN_USE', - }); - } - - await db.transaction(async (trx) => { - // Delete the configuration - await trx('combat_configurations') - .where('id', configId) - .del(); - - // Log admin action - await trx('audit_log').insert({ - entity_type: 'combat_configuration', - entity_id: configId, - action: 'delete_combat_configuration', - actor_type: 'admin', - actor_id: req.user.id, - changes: JSON.stringify({ - deleted_config: config, - }), - metadata: JSON.stringify({ - correlation_id: correlationId, - }), - ip_address: req.ip, - user_agent: req.get('User-Agent'), - }); - }); - - logger.info('Combat configuration deleted', { - correlationId, - adminUser: req.user.id, - configId, - configName: config.config_name, - }); - - res.json({ - success: true, - message: 'Combat configuration deleted successfully', - }); - - } catch (error) { - logger.error('Failed to delete combat configuration', { - correlationId: req.correlationId, - adminUser: req.user?.id, - configId: req.params.configId, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } -} - -// Export singleton instance and bound methods -const adminCombatController = new AdminCombatController(); - -module.exports = { - AdminCombatController, - - // Export bound methods for route usage - getCombatStatistics: adminCombatController.getCombatStatistics.bind(adminCombatController), - getCombatQueue: adminCombatController.getCombatQueue.bind(adminCombatController), - forceResolveCombat: adminCombatController.forceResolveCombat.bind(adminCombatController), - cancelBattle: adminCombatController.cancelBattle.bind(adminCombatController), - getCombatConfigurations: adminCombatController.getCombatConfigurations.bind(adminCombatController), - saveCombatConfiguration: adminCombatController.saveCombatConfiguration.bind(adminCombatController), - deleteCombatConfiguration: adminCombatController.deleteCombatConfiguration.bind(adminCombatController), -}; diff --git a/src/controllers/api/auth.controller.js b/src/controllers/api/auth.controller.js index f7e265e..8b870d1 100644 --- a/src/controllers/api/auth.controller.js +++ b/src/controllers/api/auth.controller.js @@ -14,222 +14,36 @@ const playerService = new PlayerService(); * POST /api/auth/register */ const register = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { email, username, password } = req.body; - const startTime = Date.now(); + const correlationId = req.correlationId; + const { email, username, password } = req.body; - logger.info('Player registration request received', { - correlationId, - email, - username, - requestSize: JSON.stringify(req.body).length, - userAgent: req.get('User-Agent'), - ipAddress: req.ip || req.connection.remoteAddress, - headers: { - contentType: req.get('Content-Type'), - contentLength: req.get('Content-Length'), - }, - }); - - try { - // Step 1: Validate input data presence - logger.debug('Validating input data', { - correlationId, - hasEmail: !!email, - hasUsername: !!username, - hasPassword: !!password, - emailLength: email?.length, - usernameLength: username?.length, - passwordLength: password?.length, - }); - - if (!email || !username || !password) { - logger.warn('Registration failed - missing required fields', { + logger.info('Player registration request received', { correlationId, - missingFields: { - email: !email, - username: !username, - password: !password, - }, - }); - return res.status(400).json({ - success: false, - error: 'Missing required fields', - message: 'Email, username, and password are required', - correlationId, - }); - } - - // Step 2: Check service dependencies - logger.debug('Checking service dependencies', { - correlationId, - playerServiceAvailable: !!playerService, - playerServiceType: typeof playerService, - }); - - if (!playerService || typeof playerService.registerPlayer !== 'function') { - logger.error('PlayerService not available or invalid', { - correlationId, - playerService: !!playerService, - registerMethod: typeof playerService?.registerPlayer, - }); - return res.status(500).json({ - success: false, - error: 'Service unavailable', - message: 'Registration service is currently unavailable', - correlationId, - }); - } - - // Step 3: Test database connectivity - logger.debug('Testing database connectivity', { correlationId }); - try { - const db = require('../../database/connection'); - await db.raw('SELECT 1 as test'); - logger.debug('Database connectivity verified', { correlationId }); - } catch (dbError) { - logger.error('Database connectivity failed', { - correlationId, - error: dbError.message, - code: dbError.code, - stack: dbError.stack, - }); - return res.status(500).json({ - success: false, - error: 'Database unavailable', - message: 'Database service is currently unavailable', - correlationId, - debug: process.env.NODE_ENV === 'development' ? { - dbError: dbError.message, - dbCode: dbError.code, - } : undefined, - }); - } - - // Step 4: Call PlayerService.registerPlayer - logger.debug('Calling PlayerService.registerPlayer', { - correlationId, - email, - username, + email, + username }); const player = await playerService.registerPlayer({ - email, - username, - password, + email, + username, + password }, correlationId); - logger.debug('PlayerService.registerPlayer completed', { - correlationId, - playerId: player?.id, - playerEmail: player?.email, - playerUsername: player?.username, - playerData: { - hasId: !!player?.id, - hasEmail: !!player?.email, - hasUsername: !!player?.username, - isActive: player?.isActive, - isVerified: player?.isVerified, - }, - }); - - // Step 5: Generate tokens for immediate login after registration - logger.debug('Initializing TokenService', { correlationId }); - const TokenService = require('../../services/auth/TokenService'); - const tokenService = new TokenService(); - - if (!tokenService || typeof tokenService.generateAuthTokens !== 'function') { - logger.error('TokenService not available or invalid', { - correlationId, - tokenService: !!tokenService, - generateMethod: typeof tokenService?.generateAuthTokens, - }); - return res.status(500).json({ - success: false, - error: 'Token service unavailable', - message: 'Authentication service is currently unavailable', - correlationId, - }); - } - - logger.debug('Generating authentication tokens', { - correlationId, - playerId: player.id, - email: player.email, - }); - - const tokens = await tokenService.generateAuthTokens({ - id: player.id, - email: player.email, - username: player.username, - userAgent: req.get('User-Agent'), - ipAddress: req.ip || req.connection.remoteAddress, - }); - - logger.debug('Authentication tokens generated', { - correlationId, - hasAccessToken: !!tokens?.accessToken, - hasRefreshToken: !!tokens?.refreshToken, - accessTokenLength: tokens?.accessToken?.length, - refreshTokenLength: tokens?.refreshToken?.length, - }); - - // Step 6: Set refresh token as httpOnly cookie - logger.debug('Setting refresh token cookie', { correlationId }); - res.cookie('refreshToken', tokens.refreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days - }); - - // Step 7: Prepare and send response - const responseData = { - success: true, - message: 'Player registered successfully', - data: { - user: player, // Frontend expects 'user' not 'player' - token: tokens.accessToken, // Frontend expects 'token' not 'accessToken' - }, - correlationId, - }; - - const duration = Date.now() - startTime; logger.info('Player registration successful', { - correlationId, - playerId: player.id, - email: player.email, - username: player.username, - duration: `${duration}ms`, - responseSize: JSON.stringify(responseData).length, + correlationId, + playerId: player.id, + email: player.email, + username: player.username }); - res.status(201).json(responseData); - - } catch (error) { - const duration = Date.now() - startTime; - logger.error('Player registration failed with error', { - correlationId, - error: error.message, - errorName: error.name, - errorStack: error.stack, - statusCode: error.statusCode, - duration: `${duration}ms`, - email, - username, - requestBody: { - hasEmail: !!email, - hasUsername: !!username, - hasPassword: !!password, - emailValid: email && email.includes('@'), - usernameLength: username?.length, - passwordLength: password?.length, - }, + res.status(201).json({ + success: true, + message: 'Player registered successfully', + data: { + player + }, + correlationId }); - - // Re-throw to let error middleware handle it - throw error; - } }); /** @@ -237,45 +51,43 @@ const register = asyncHandler(async (req, res) => { * POST /api/auth/login */ const login = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { email, password } = req.body; + const correlationId = req.correlationId; + const { email, password } = req.body; - logger.info('Player login request received', { - correlationId, - email, - }); + logger.info('Player login request received', { + correlationId, + email + }); - const authResult = await playerService.authenticatePlayer({ - email, - password, - ipAddress: req.ip || req.connection.remoteAddress, - userAgent: req.get('User-Agent'), - }, correlationId); + const authResult = await playerService.authenticatePlayer({ + email, + password + }, correlationId); - logger.info('Player login successful', { - correlationId, - playerId: authResult.player.id, - email: authResult.player.email, - username: authResult.player.username, - }); + logger.info('Player login successful', { + correlationId, + playerId: authResult.player.id, + email: authResult.player.email, + username: authResult.player.username + }); - // Set refresh token as httpOnly cookie - res.cookie('refreshToken', authResult.tokens.refreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days - }); + // Set refresh token as httpOnly cookie + res.cookie('refreshToken', authResult.tokens.refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days + }); - res.status(200).json({ - success: true, - message: 'Login successful', - data: { - user: authResult.player, - token: authResult.tokens.accessToken, - }, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Login successful', + data: { + player: authResult.player, + accessToken: authResult.tokens.accessToken + }, + correlationId + }); }); /** @@ -283,53 +95,29 @@ const login = asyncHandler(async (req, res) => { * POST /api/auth/logout */ const logout = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user?.playerId; + const correlationId = req.correlationId; + const playerId = req.user?.playerId; - logger.info('Player logout request received', { - correlationId, - playerId, - }); + logger.info('Player logout request received', { + correlationId, + playerId + }); - // Clear refresh token cookie - res.clearCookie('refreshToken'); + // Clear refresh token cookie + res.clearCookie('refreshToken'); - // Blacklist the access token if available - const authHeader = req.headers.authorization; - if (authHeader) { - const { extractTokenFromHeader } = require('../../utils/jwt'); - const accessToken = extractTokenFromHeader(authHeader); - - if (accessToken) { - const TokenService = require('../../services/auth/TokenService'); - const tokenService = new TokenService(); - - try { - await tokenService.blacklistToken(accessToken, 'logout'); - logger.info('Access token blacklisted', { - correlationId, - playerId, - }); - } catch (error) { - logger.warn('Failed to blacklist token on logout', { - correlationId, - playerId, - error: error.message, - }); - } - } - } + // TODO: Add token to blacklist if implementing token blacklisting - logger.info('Player logout successful', { - correlationId, - playerId, - }); + logger.info('Player logout successful', { + correlationId, + playerId + }); - res.status(200).json({ - success: true, - message: 'Logout successful', - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Logout successful', + correlationId + }); }); /** @@ -337,37 +125,32 @@ const logout = asyncHandler(async (req, res) => { * POST /api/auth/refresh */ const refresh = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const refreshToken = req.cookies.refreshToken; + const correlationId = req.correlationId; + const refreshToken = req.cookies.refreshToken; - if (!refreshToken) { - logger.warn('Token refresh request without refresh token', { - correlationId, + if (!refreshToken) { + logger.warn('Token refresh request without refresh token', { + correlationId + }); + + return res.status(401).json({ + success: false, + message: 'Refresh token not provided', + correlationId + }); + } + + // TODO: Implement refresh token validation and new token generation + // For now, return error indicating feature not implemented + logger.warn('Token refresh requested but not implemented', { + correlationId }); - return res.status(401).json({ - success: false, - message: 'Refresh token not provided', - correlationId, + res.status(501).json({ + success: false, + message: 'Token refresh feature not yet implemented', + correlationId }); - } - - logger.info('Token refresh request received', { - correlationId, - }); - - const result = await playerService.refreshAccessToken(refreshToken, correlationId); - - res.status(200).json({ - success: true, - message: 'Token refreshed successfully', - data: { - accessToken: result.accessToken, - playerId: result.playerId, - email: result.email, - }, - correlationId, - }); }); /** @@ -375,30 +158,30 @@ const refresh = asyncHandler(async (req, res) => { * GET /api/auth/me */ const getProfile = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; + const correlationId = req.correlationId; + const playerId = req.user.playerId; - logger.info('Player profile request received', { - correlationId, - playerId, - }); + logger.info('Player profile request received', { + correlationId, + playerId + }); - const profile = await playerService.getPlayerProfile(playerId, correlationId); + const profile = await playerService.getPlayerProfile(playerId, correlationId); - logger.info('Player profile retrieved', { - correlationId, - playerId, - username: profile.username, - }); + logger.info('Player profile retrieved', { + correlationId, + playerId, + username: profile.username + }); - res.status(200).json({ - success: true, - message: 'Profile retrieved successfully', - data: { - player: profile, - }, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Profile retrieved successfully', + data: { + player: profile + }, + correlationId + }); }); /** @@ -406,36 +189,36 @@ const getProfile = asyncHandler(async (req, res) => { * PUT /api/auth/me */ const updateProfile = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const updateData = req.body; + const correlationId = req.correlationId; + const playerId = req.user.playerId; + const updateData = req.body; - logger.info('Player profile update request received', { - correlationId, - playerId, - updateFields: Object.keys(updateData), - }); + logger.info('Player profile update request received', { + correlationId, + playerId, + updateFields: Object.keys(updateData) + }); - const updatedProfile = await playerService.updatePlayerProfile( - playerId, - updateData, - correlationId, - ); + const updatedProfile = await playerService.updatePlayerProfile( + playerId, + updateData, + correlationId + ); - logger.info('Player profile updated successfully', { - correlationId, - playerId, - username: updatedProfile.username, - }); + logger.info('Player profile updated successfully', { + correlationId, + playerId, + username: updatedProfile.username + }); - res.status(200).json({ - success: true, - message: 'Profile updated successfully', - data: { - player: updatedProfile, - }, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Profile updated successfully', + data: { + player: updatedProfile + }, + correlationId + }); }); /** @@ -443,30 +226,30 @@ const updateProfile = asyncHandler(async (req, res) => { * GET /api/auth/verify */ const verifyToken = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const user = req.user; + const correlationId = req.correlationId; + const user = req.user; - logger.info('Token verification request received', { - correlationId, - playerId: user.playerId, - username: user.username, - }); - - res.status(200).json({ - success: true, - message: 'Token is valid', - data: { - user: { + logger.info('Token verification request received', { + correlationId, playerId: user.playerId, - email: user.email, - username: user.username, - type: user.type, - tokenIssuedAt: new Date(user.iat * 1000), - tokenExpiresAt: new Date(user.exp * 1000), - }, - }, - correlationId, - }); + username: user.username + }); + + res.status(200).json({ + success: true, + message: 'Token is valid', + data: { + user: { + playerId: user.playerId, + email: user.email, + username: user.username, + type: user.type, + tokenIssuedAt: new Date(user.iat * 1000), + tokenExpiresAt: new Date(user.exp * 1000) + } + }, + correlationId + }); }); /** @@ -474,500 +257,42 @@ const verifyToken = asyncHandler(async (req, res) => { * POST /api/auth/change-password */ const changePassword = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const { currentPassword, newPassword } = req.body; + const correlationId = req.correlationId; + const playerId = req.user.playerId; + const { currentPassword, newPassword } = req.body; - logger.info('Password change request received', { - correlationId, - playerId, - }); - - const result = await playerService.changePassword( - playerId, - currentPassword, - newPassword, - correlationId - ); - - logger.info('Password changed successfully', { - correlationId, - playerId, - }); - - res.status(200).json({ - success: true, - message: result.message, - correlationId, - }); -}); - -/** - * Verify email address - * POST /api/auth/verify-email - */ -const verifyEmail = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { token } = req.body; - - logger.info('Email verification request received', { - correlationId, - tokenPrefix: token.substring(0, 8) + '...', - }); - - const result = await playerService.verifyEmail(token, correlationId); - - logger.info('Email verification completed', { - correlationId, - success: result.success, - }); - - res.status(200).json({ - success: result.success, - message: result.message, - data: result.player ? { player: result.player } : undefined, - correlationId, - }); -}); - -/** - * Resend email verification - * POST /api/auth/resend-verification - */ -const resendVerification = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { email } = req.body; - - logger.info('Resend verification request received', { - correlationId, - email, - }); - - const result = await playerService.resendEmailVerification(email, correlationId); - - res.status(200).json({ - success: result.success, - message: result.message, - correlationId, - }); -}); - -/** - * Request password reset - * POST /api/auth/request-password-reset - */ -const requestPasswordReset = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { email } = req.body; - - logger.info('Password reset request received', { - correlationId, - email, - }); - - const result = await playerService.requestPasswordReset(email, correlationId); - - res.status(200).json({ - success: result.success, - message: result.message, - correlationId, - }); -}); - -/** - * Reset password using token - * POST /api/auth/reset-password - */ -const resetPassword = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { token, newPassword } = req.body; - - logger.info('Password reset completion request received', { - correlationId, - tokenPrefix: token.substring(0, 8) + '...', - }); - - const result = await playerService.resetPassword(token, newPassword, correlationId); - - logger.info('Password reset completed successfully', { - correlationId, - }); - - res.status(200).json({ - success: result.success, - message: result.message, - correlationId, - }); -}); - -/** - * Registration diagnostic endpoint (development only) - * GET /api/auth/debug/registration-test - */ -const registrationDiagnostic = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const startTime = Date.now(); - - // Only available in development - if (process.env.NODE_ENV === 'production') { - return res.status(404).json({ - success: false, - message: 'Not found', - correlationId, - }); - } - - logger.info('Registration diagnostic requested', { correlationId }); - - const diagnostics = { - timestamp: new Date().toISOString(), - correlationId, - environment: process.env.NODE_ENV, - tests: {}, - services: {}, - database: {}, - overall: { status: 'unknown', errors: [] }, - }; - - try { - // Test 1: Database connectivity - logger.debug('Testing database connectivity', { correlationId }); - try { - const db = require('../../database/connection'); - const testResult = await db.raw('SELECT 1 as test, NOW() as timestamp'); - diagnostics.database = { - status: 'connected', - testQuery: 'SELECT 1 as test, NOW() as timestamp', - result: testResult.rows[0], - connection: { - host: db.client.config.connection.host, - database: db.client.config.connection.database, - port: db.client.config.connection.port, - }, - }; - diagnostics.tests.database = 'PASS'; - } catch (dbError) { - diagnostics.database = { - status: 'error', - error: dbError.message, - code: dbError.code, - }; - diagnostics.tests.database = 'FAIL'; - diagnostics.overall.errors.push(`Database: ${dbError.message}`); - } - - // Test 2: Required tables exist - logger.debug('Testing required tables exist', { correlationId }); - try { - const db = require('../../database/connection'); - const requiredTables = ['players', 'player_stats', 'player_resources']; - const tableTests = {}; - - for (const table of requiredTables) { - try { - const exists = await db.schema.hasTable(table); - tableTests[table] = exists ? 'EXISTS' : 'MISSING'; - if (!exists) { - diagnostics.overall.errors.push(`Table missing: ${table}`); - } - } catch (error) { - tableTests[table] = `ERROR: ${error.message}`; - diagnostics.overall.errors.push(`Table check failed for ${table}: ${error.message}`); - } - } - diagnostics.database.tables = tableTests; - diagnostics.tests.requiredTables = Object.values(tableTests).every(status => status === 'EXISTS') ? 'PASS' : 'FAIL'; - } catch (error) { - diagnostics.database.tables = { error: error.message }; - diagnostics.tests.requiredTables = 'FAIL'; - diagnostics.overall.errors.push(`Table check failed: ${error.message}`); - } - - // Test 3: PlayerService availability - logger.debug('Testing PlayerService availability', { correlationId }); - try { - const serviceAvailable = !!playerService && typeof playerService.registerPlayer === 'function'; - diagnostics.services.playerService = { - available: serviceAvailable, - hasRegisterMethod: typeof playerService?.registerPlayer === 'function', - type: typeof playerService, - methods: playerService ? Object.getOwnPropertyNames(Object.getPrototypeOf(playerService)).filter(name => name !== 'constructor') : [], - }; - diagnostics.tests.playerService = serviceAvailable ? 'PASS' : 'FAIL'; - if (!serviceAvailable) { - diagnostics.overall.errors.push('PlayerService not available or missing registerPlayer method'); - } - } catch (error) { - diagnostics.services.playerService = { error: error.message }; - diagnostics.tests.playerService = 'FAIL'; - diagnostics.overall.errors.push(`PlayerService test failed: ${error.message}`); - } - - // Test 4: TokenService availability - logger.debug('Testing TokenService availability', { correlationId }); - try { - const TokenService = require('../../services/auth/TokenService'); - const tokenService = new TokenService(); - const serviceAvailable = !!tokenService && typeof tokenService.generateAuthTokens === 'function'; - diagnostics.services.tokenService = { - available: serviceAvailable, - hasGenerateMethod: typeof tokenService?.generateAuthTokens === 'function', - type: typeof tokenService, - methods: tokenService ? Object.getOwnPropertyNames(Object.getPrototypeOf(tokenService)).filter(name => name !== 'constructor') : [], - }; - diagnostics.tests.tokenService = serviceAvailable ? 'PASS' : 'FAIL'; - if (!serviceAvailable) { - diagnostics.overall.errors.push('TokenService not available or missing generateAuthTokens method'); - } - } catch (error) { - diagnostics.services.tokenService = { error: error.message }; - diagnostics.tests.tokenService = 'FAIL'; - diagnostics.overall.errors.push(`TokenService test failed: ${error.message}`); - } - - // Test 5: Redis availability (if used) - logger.debug('Testing Redis availability', { correlationId }); - try { - // Check if Redis client is available in TokenService - const TokenService = require('../../services/auth/TokenService'); - const tokenService = new TokenService(); - if (tokenService.redisClient) { - const pingResult = await tokenService.redisClient.ping(); - diagnostics.services.redis = { - available: true, - pingResult, - status: 'connected', - }; - diagnostics.tests.redis = 'PASS'; - } else { - diagnostics.services.redis = { - available: false, - status: 'not_configured', - }; - diagnostics.tests.redis = 'SKIP'; - } - } catch (error) { - diagnostics.services.redis = { - available: false, - error: error.message, - status: 'error', - }; - diagnostics.tests.redis = 'FAIL'; - diagnostics.overall.errors.push(`Redis test failed: ${error.message}`); - } - - // Test 6: Validation utilities - logger.debug('Testing validation utilities', { correlationId }); - try { - const { validateEmail, validateUsername } = require('../../utils/validation'); - const { validatePasswordStrength } = require('../../utils/security'); - - const validationTests = { - email: typeof validateEmail === 'function', - username: typeof validateUsername === 'function', - password: typeof validatePasswordStrength === 'function', - }; - - diagnostics.services.validation = { - available: Object.values(validationTests).every(test => test), - functions: validationTests, - }; - diagnostics.tests.validation = Object.values(validationTests).every(test => test) ? 'PASS' : 'FAIL'; - - if (!Object.values(validationTests).every(test => test)) { - diagnostics.overall.errors.push('Validation utilities missing or invalid'); - } - } catch (error) { - diagnostics.services.validation = { error: error.message }; - diagnostics.tests.validation = 'FAIL'; - diagnostics.overall.errors.push(`Validation test failed: ${error.message}`); - } - - // Test 7: Password hashing utilities - logger.debug('Testing password utilities', { correlationId }); - try { - const { hashPassword, verifyPassword } = require('../../utils/password'); - - const passwordTests = { - hashPassword: typeof hashPassword === 'function', - verifyPassword: typeof verifyPassword === 'function', - }; - - diagnostics.services.passwordUtils = { - available: Object.values(passwordTests).every(test => test), - functions: passwordTests, - }; - diagnostics.tests.passwordUtils = Object.values(passwordTests).every(test => test) ? 'PASS' : 'FAIL'; - - if (!Object.values(passwordTests).every(test => test)) { - diagnostics.overall.errors.push('Password utilities missing or invalid'); - } - } catch (error) { - diagnostics.services.passwordUtils = { error: error.message }; - diagnostics.tests.passwordUtils = 'FAIL'; - diagnostics.overall.errors.push(`Password utilities test failed: ${error.message}`); - } - - // Determine overall status - const failedTests = Object.values(diagnostics.tests).filter(status => status === 'FAIL').length; - const totalTests = Object.values(diagnostics.tests).length; - - if (failedTests === 0) { - diagnostics.overall.status = 'healthy'; - } else if (failedTests < totalTests) { - diagnostics.overall.status = 'degraded'; - } else { - diagnostics.overall.status = 'unhealthy'; - } - - const duration = Date.now() - startTime; - diagnostics.duration = `${duration}ms`; - - logger.info('Registration diagnostic completed', { - correlationId, - status: diagnostics.overall.status, - failedTests, - totalTests, - duration: diagnostics.duration, - errors: diagnostics.overall.errors, + logger.info('Password change request received', { + correlationId, + playerId }); - res.status(200).json({ - success: true, - message: 'Registration diagnostic completed', - data: diagnostics, - correlationId, + // TODO: Implement password change functionality + // This would involve: + // 1. Verify current password + // 2. Validate new password strength + // 3. Hash new password + // 4. Update in database + // 5. Optionally invalidate existing tokens + + logger.warn('Password change requested but not implemented', { + correlationId, + playerId }); - } catch (error) { - const duration = Date.now() - startTime; - logger.error('Registration diagnostic failed', { - correlationId, - error: error.message, - stack: error.stack, - duration: `${duration}ms`, + res.status(501).json({ + success: false, + message: 'Password change feature not yet implemented', + correlationId }); - - diagnostics.overall = { - status: 'error', - error: error.message, - duration: `${duration}ms`, - }; - - res.status(500).json({ - success: false, - message: 'Diagnostic test failed', - data: diagnostics, - error: error.message, - correlationId, - }); - } -}); - -/** - * Check password strength - * POST /api/auth/check-password-strength - */ -const checkPasswordStrength = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { password } = req.body; - - if (!password) { - return res.status(400).json({ - success: false, - message: 'Password is required', - correlationId, - }); - } - - const { validatePasswordStrength } = require('../../utils/security'); - const validation = validatePasswordStrength(password); - - res.status(200).json({ - success: true, - message: 'Password strength evaluated', - data: { - isValid: validation.isValid, - errors: validation.errors, - requirements: validation.requirements, - strength: validation.strength, - }, - correlationId, - }); -}); - -/** - * Get security status - * GET /api/auth/security-status - */ -const getSecurityStatus = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - - logger.info('Security status request received', { - correlationId, - playerId, - }); - - // Get player security information - const db = require('../../database/connection'); - const player = await db('players') - .select([ - 'id', - 'email', - 'username', - 'email_verified', - 'is_active', - 'is_banned', - 'last_login', - 'created_at', - ]) - .where('id', playerId) - .first(); - - if (!player) { - return res.status(404).json({ - success: false, - message: 'Player not found', - correlationId, - }); - } - - const securityStatus = { - emailVerified: player.email_verified, - accountActive: player.is_active, - accountBanned: player.is_banned, - lastLogin: player.last_login, - accountAge: Math.floor((Date.now() - new Date(player.created_at).getTime()) / (1000 * 60 * 60 * 24)), - securityFeatures: { - twoFactorEnabled: false, // TODO: Implement 2FA - securityNotifications: true, - loginNotifications: true, - }, - }; - - res.status(200).json({ - success: true, - message: 'Security status retrieved', - data: { securityStatus }, - correlationId, - }); }); module.exports = { - register, - login, - logout, - refresh, - getProfile, - updateProfile, - verifyToken, - changePassword, - verifyEmail, - resendVerification, - requestPasswordReset, - resetPassword, - checkPasswordStrength, - getSecurityStatus, - registrationDiagnostic, -}; + register, + login, + logout, + refresh, + getProfile, + updateProfile, + verifyToken, + changePassword +}; \ No newline at end of file diff --git a/src/controllers/api/auth.controller.js.backup b/src/controllers/api/auth.controller.js.backup deleted file mode 100644 index c154451..0000000 --- a/src/controllers/api/auth.controller.js.backup +++ /dev/null @@ -1,543 +0,0 @@ -/** - * Player Authentication Controller - * Handles player registration, login, and authentication-related endpoints - */ - -const PlayerService = require('../../services/user/PlayerService'); -const { asyncHandler } = require('../../middleware/error.middleware'); -const logger = require('../../utils/logger'); - -const playerService = new PlayerService(); - -/** - * Register a new player - * POST /api/auth/register - */ -const register = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { email, username, password } = req.body; - - logger.info('Player registration request received', { - correlationId, - email, - username, - }); - - const player = await playerService.registerPlayer({ - email, - username, - password, - }, correlationId); - - // Generate tokens for immediate login after registration - const TokenService = require('../../services/auth/TokenService'); - const tokenService = new TokenService(); - - const tokens = await tokenService.generateAuthTokens({ - id: player.id, - email: player.email, - username: player.username, - userAgent: req.get('User-Agent'), - ipAddress: req.ip || req.connection.remoteAddress, - }); - - logger.info('Player registration successful', { - correlationId, - playerId: player.id, - email: player.email, - username: player.username, - }); - - // Set refresh token as httpOnly cookie - res.cookie('refreshToken', tokens.refreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days - }); - - res.status(201).json({ - success: true, - message: 'Player registered successfully', - data: { - user: player, // Frontend expects 'user' not 'player' - token: tokens.accessToken, // Frontend expects 'token' not 'accessToken' - }, - correlationId, - }); -}); - -/** - * Player login - * POST /api/auth/login - */ -const login = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { email, password } = req.body; - - logger.info('Player login request received', { - correlationId, - email, - }); - - const authResult = await playerService.authenticatePlayer({ - email, - password, - ipAddress: req.ip || req.connection.remoteAddress, - userAgent: req.get('User-Agent'), - }, correlationId); - - logger.info('Player login successful', { - correlationId, - playerId: authResult.player.id, - email: authResult.player.email, - username: authResult.player.username, - }); - - // Set refresh token as httpOnly cookie - res.cookie('refreshToken', authResult.tokens.refreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days - }); - - res.status(200).json({ - success: true, - message: 'Login successful', - data: { - player: authResult.player, - accessToken: authResult.tokens.accessToken, - }, - correlationId, - }); -}); - -/** - * Player logout - * POST /api/auth/logout - */ -const logout = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user?.playerId; - - logger.info('Player logout request received', { - correlationId, - playerId, - }); - - // Clear refresh token cookie - res.clearCookie('refreshToken'); - - // Blacklist the access token if available - const authHeader = req.headers.authorization; - if (authHeader) { - const { extractTokenFromHeader } = require('../../utils/jwt'); - const accessToken = extractTokenFromHeader(authHeader); - - if (accessToken) { - const TokenService = require('../../services/auth/TokenService'); - const tokenService = new TokenService(); - - try { - await tokenService.blacklistToken(accessToken, 'logout'); - logger.info('Access token blacklisted', { - correlationId, - playerId, - }); - } catch (error) { - logger.warn('Failed to blacklist token on logout', { - correlationId, - playerId, - error: error.message, - }); - } - } - } - - logger.info('Player logout successful', { - correlationId, - playerId, - }); - - res.status(200).json({ - success: true, - message: 'Logout successful', - correlationId, - }); -}); - -/** - * Refresh access token - * POST /api/auth/refresh - */ -const refresh = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const refreshToken = req.cookies.refreshToken; - - if (!refreshToken) { - logger.warn('Token refresh request without refresh token', { - correlationId, - }); - - return res.status(401).json({ - success: false, - message: 'Refresh token not provided', - correlationId, - }); - } - - logger.info('Token refresh request received', { - correlationId, - }); - - const result = await playerService.refreshAccessToken(refreshToken, correlationId); - - res.status(200).json({ - success: true, - message: 'Token refreshed successfully', - data: { - accessToken: result.accessToken, - playerId: result.playerId, - email: result.email, - }, - correlationId, - }); -}); - -/** - * Get current player profile - * GET /api/auth/me - */ -const getProfile = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - - logger.info('Player profile request received', { - correlationId, - playerId, - }); - - const profile = await playerService.getPlayerProfile(playerId, correlationId); - - logger.info('Player profile retrieved', { - correlationId, - playerId, - username: profile.username, - }); - - res.status(200).json({ - success: true, - message: 'Profile retrieved successfully', - data: { - player: profile, - }, - correlationId, - }); -}); - -/** - * Update current player profile - * PUT /api/auth/me - */ -const updateProfile = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const updateData = req.body; - - logger.info('Player profile update request received', { - correlationId, - playerId, - updateFields: Object.keys(updateData), - }); - - const updatedProfile = await playerService.updatePlayerProfile( - playerId, - updateData, - correlationId, - ); - - logger.info('Player profile updated successfully', { - correlationId, - playerId, - username: updatedProfile.username, - }); - - res.status(200).json({ - success: true, - message: 'Profile updated successfully', - data: { - player: updatedProfile, - }, - correlationId, - }); -}); - -/** - * Verify player token (for testing/debugging) - * GET /api/auth/verify - */ -const verifyToken = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const user = req.user; - - logger.info('Token verification request received', { - correlationId, - playerId: user.playerId, - username: user.username, - }); - - res.status(200).json({ - success: true, - message: 'Token is valid', - data: { - user: { - playerId: user.playerId, - email: user.email, - username: user.username, - type: user.type, - tokenIssuedAt: new Date(user.iat * 1000), - tokenExpiresAt: new Date(user.exp * 1000), - }, - }, - correlationId, - }); -}); - -/** - * Change player password - * POST /api/auth/change-password - */ -const changePassword = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const { currentPassword, newPassword } = req.body; - - logger.info('Password change request received', { - correlationId, - playerId, - }); - - const result = await playerService.changePassword( - playerId, - currentPassword, - newPassword, - correlationId - ); - - logger.info('Password changed successfully', { - correlationId, - playerId, - }); - - res.status(200).json({ - success: true, - message: result.message, - correlationId, - }); -}); - -/** - * Verify email address - * POST /api/auth/verify-email - */ -const verifyEmail = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { token } = req.body; - - logger.info('Email verification request received', { - correlationId, - tokenPrefix: token.substring(0, 8) + '...', - }); - - const result = await playerService.verifyEmail(token, correlationId); - - logger.info('Email verification completed', { - correlationId, - success: result.success, - }); - - res.status(200).json({ - success: result.success, - message: result.message, - data: result.player ? { player: result.player } : undefined, - correlationId, - }); -}); - -/** - * Resend email verification - * POST /api/auth/resend-verification - */ -const resendVerification = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { email } = req.body; - - logger.info('Resend verification request received', { - correlationId, - email, - }); - - const result = await playerService.resendEmailVerification(email, correlationId); - - res.status(200).json({ - success: result.success, - message: result.message, - correlationId, - }); -}); - -/** - * Request password reset - * POST /api/auth/request-password-reset - */ -const requestPasswordReset = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { email } = req.body; - - logger.info('Password reset request received', { - correlationId, - email, - }); - - const result = await playerService.requestPasswordReset(email, correlationId); - - res.status(200).json({ - success: result.success, - message: result.message, - correlationId, - }); -}); - -/** - * Reset password using token - * POST /api/auth/reset-password - */ -const resetPassword = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { token, newPassword } = req.body; - - logger.info('Password reset completion request received', { - correlationId, - tokenPrefix: token.substring(0, 8) + '...', - }); - - const result = await playerService.resetPassword(token, newPassword, correlationId); - - logger.info('Password reset completed successfully', { - correlationId, - }); - - res.status(200).json({ - success: result.success, - message: result.message, - correlationId, - }); -}); - -/** - * Check password strength - * POST /api/auth/check-password-strength - */ -const checkPasswordStrength = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const { password } = req.body; - - if (!password) { - return res.status(400).json({ - success: false, - message: 'Password is required', - correlationId, - }); - } - - const { validatePasswordStrength } = require('../../utils/security'); - const validation = validatePasswordStrength(password); - - res.status(200).json({ - success: true, - message: 'Password strength evaluated', - data: { - isValid: validation.isValid, - errors: validation.errors, - requirements: validation.requirements, - strength: validation.strength, - }, - correlationId, - }); -}); - -/** - * Get security status - * GET /api/auth/security-status - */ -const getSecurityStatus = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - - logger.info('Security status request received', { - correlationId, - playerId, - }); - - // Get player security information - const db = require('../../database/connection'); - const player = await db('players') - .select([ - 'id', - 'email', - 'username', - 'email_verified', - 'is_active', - 'is_banned', - 'last_login', - 'created_at', - ]) - .where('id', playerId) - .first(); - - if (!player) { - return res.status(404).json({ - success: false, - message: 'Player not found', - correlationId, - }); - } - - const securityStatus = { - emailVerified: player.email_verified, - accountActive: player.is_active, - accountBanned: player.is_banned, - lastLogin: player.last_login, - accountAge: Math.floor((Date.now() - new Date(player.created_at).getTime()) / (1000 * 60 * 60 * 24)), - securityFeatures: { - twoFactorEnabled: false, // TODO: Implement 2FA - securityNotifications: true, - loginNotifications: true, - }, - }; - - res.status(200).json({ - success: true, - message: 'Security status retrieved', - data: { securityStatus }, - correlationId, - }); -}); - -module.exports = { - register, - login, - logout, - refresh, - getProfile, - updateProfile, - verifyToken, - changePassword, - verifyEmail, - resendVerification, - requestPasswordReset, - resetPassword, - checkPasswordStrength, - getSecurityStatus, -}; diff --git a/src/controllers/api/combat.controller.js b/src/controllers/api/combat.controller.js deleted file mode 100644 index dfe3e11..0000000 --- a/src/controllers/api/combat.controller.js +++ /dev/null @@ -1,572 +0,0 @@ -/** - * Combat API Controller - * Handles all combat-related HTTP requests including combat initiation, status, and history - */ - -const CombatService = require('../../services/combat/CombatService'); -const { CombatPluginManager } = require('../../services/combat/CombatPluginManager'); -const GameEventService = require('../../services/websocket/GameEventService'); -const logger = require('../../utils/logger'); -const { ValidationError, ConflictError, NotFoundError } = require('../../middleware/error.middleware'); - -class CombatController { - constructor() { - this.combatPluginManager = null; - this.gameEventService = null; - this.combatService = null; - } - - /** - * Initialize controller with dependencies - * @param {Object} dependencies - Service dependencies - */ - async initialize(dependencies = {}) { - this.gameEventService = dependencies.gameEventService || new GameEventService(); - this.combatPluginManager = dependencies.combatPluginManager || new CombatPluginManager(); - this.combatService = dependencies.combatService || new CombatService(this.gameEventService, this.combatPluginManager); - - // Initialize plugin manager - await this.combatPluginManager.initialize('controller-init'); - } - - /** - * Initiate combat between fleets or fleet vs colony - * POST /api/combat/initiate - */ - async initiateCombat(req, res, next) { - try { - const correlationId = req.correlationId; - const playerId = req.user.id; - const combatData = req.body; - - logger.info('Combat initiation request', { - correlationId, - playerId, - combatData, - }); - - // Validate required fields - if (!combatData.attacker_fleet_id) { - return res.status(400).json({ - error: 'Attacker fleet ID is required', - code: 'MISSING_ATTACKER_FLEET', - }); - } - - if (!combatData.location) { - return res.status(400).json({ - error: 'Combat location is required', - code: 'MISSING_LOCATION', - }); - } - - if (!combatData.defender_fleet_id && !combatData.defender_colony_id) { - return res.status(400).json({ - error: 'Either defender fleet or colony must be specified', - code: 'MISSING_DEFENDER', - }); - } - - // Initialize services if not already done - if (!this.combatService) { - await this.initialize(); - } - - // Initiate combat - const result = await this.combatService.initiateCombat(combatData, playerId, correlationId); - - logger.info('Combat initiated successfully', { - correlationId, - playerId, - battleId: result.battleId, - }); - - res.status(201).json({ - success: true, - data: result, - message: 'Combat initiated successfully', - }); - - } catch (error) { - logger.error('Combat initiation failed', { - correlationId: req.correlationId, - playerId: req.user?.id, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError) { - return res.status(400).json({ - error: error.message, - code: 'VALIDATION_ERROR', - }); - } - - if (error instanceof ConflictError) { - return res.status(409).json({ - error: error.message, - code: 'CONFLICT_ERROR', - }); - } - - if (error instanceof NotFoundError) { - return res.status(404).json({ - error: error.message, - code: 'NOT_FOUND', - }); - } - - next(error); - } - } - - /** - * Get active combats for the current player - * GET /api/combat/active - */ - async getActiveCombats(req, res, next) { - try { - const correlationId = req.correlationId; - const playerId = req.user.id; - - logger.info('Active combats request', { - correlationId, - playerId, - }); - - if (!this.combatService) { - await this.initialize(); - } - - const activeCombats = await this.combatService.getActiveCombats(playerId, correlationId); - - logger.info('Active combats retrieved', { - correlationId, - playerId, - count: activeCombats.length, - }); - - res.json({ - success: true, - data: { - combats: activeCombats, - count: activeCombats.length, - }, - }); - - } catch (error) { - logger.error('Failed to get active combats', { - correlationId: req.correlationId, - playerId: req.user?.id, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } - - /** - * Get combat history for the current player - * GET /api/combat/history - */ - async getCombatHistory(req, res, next) { - try { - const correlationId = req.correlationId; - const playerId = req.user.id; - - // Parse query parameters - const options = { - limit: parseInt(req.query.limit) || 20, - offset: parseInt(req.query.offset) || 0, - outcome: req.query.outcome || null, - }; - - // Validate parameters - if (options.limit > 100) { - return res.status(400).json({ - error: 'Limit cannot exceed 100', - code: 'INVALID_LIMIT', - }); - } - - if (options.outcome && !['attacker_victory', 'defender_victory', 'draw'].includes(options.outcome)) { - return res.status(400).json({ - error: 'Invalid outcome filter', - code: 'INVALID_OUTCOME', - }); - } - - logger.info('Combat history request', { - correlationId, - playerId, - options, - }); - - if (!this.combatService) { - await this.initialize(); - } - - const history = await this.combatService.getCombatHistory(playerId, options, correlationId); - - logger.info('Combat history retrieved', { - correlationId, - playerId, - count: history.combats.length, - total: history.pagination.total, - }); - - res.json({ - success: true, - data: history, - }); - - } catch (error) { - logger.error('Failed to get combat history', { - correlationId: req.correlationId, - playerId: req.user?.id, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } - - /** - * Get detailed combat encounter information - * GET /api/combat/encounter/:encounterId - */ - async getCombatEncounter(req, res, next) { - try { - const correlationId = req.correlationId; - const playerId = req.user.id; - const encounterId = parseInt(req.params.encounterId); - - if (!encounterId || isNaN(encounterId)) { - return res.status(400).json({ - error: 'Valid encounter ID is required', - code: 'INVALID_ENCOUNTER_ID', - }); - } - - logger.info('Combat encounter request', { - correlationId, - playerId, - encounterId, - }); - - if (!this.combatService) { - await this.initialize(); - } - - const encounter = await this.combatService.getCombatEncounter(encounterId, playerId, correlationId); - - if (!encounter) { - return res.status(404).json({ - error: 'Combat encounter not found or access denied', - code: 'ENCOUNTER_NOT_FOUND', - }); - } - - logger.info('Combat encounter retrieved', { - correlationId, - playerId, - encounterId, - }); - - res.json({ - success: true, - data: encounter, - }); - - } catch (error) { - logger.error('Failed to get combat encounter', { - correlationId: req.correlationId, - playerId: req.user?.id, - encounterId: req.params.encounterId, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } - - /** - * Get combat statistics for the current player - * GET /api/combat/statistics - */ - async getCombatStatistics(req, res, next) { - try { - const correlationId = req.correlationId; - const playerId = req.user.id; - - logger.info('Combat statistics request', { - correlationId, - playerId, - }); - - if (!this.combatService) { - await this.initialize(); - } - - const statistics = await this.combatService.getCombatStatistics(playerId, correlationId); - - logger.info('Combat statistics retrieved', { - correlationId, - playerId, - }); - - res.json({ - success: true, - data: statistics, - }); - - } catch (error) { - logger.error('Failed to get combat statistics', { - correlationId: req.correlationId, - playerId: req.user?.id, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } - - /** - * Update fleet positioning for tactical combat - * PUT /api/combat/position/:fleetId - */ - async updateFleetPosition(req, res, next) { - try { - const correlationId = req.correlationId; - const playerId = req.user.id; - const fleetId = parseInt(req.params.fleetId); - const positionData = req.body; - - if (!fleetId || isNaN(fleetId)) { - return res.status(400).json({ - error: 'Valid fleet ID is required', - code: 'INVALID_FLEET_ID', - }); - } - - logger.info('Fleet position update request', { - correlationId, - playerId, - fleetId, - positionData, - }); - - if (!this.combatService) { - await this.initialize(); - } - - const result = await this.combatService.updateFleetPosition(fleetId, positionData, playerId, correlationId); - - logger.info('Fleet position updated', { - correlationId, - playerId, - fleetId, - }); - - res.json({ - success: true, - data: result, - message: 'Fleet position updated successfully', - }); - - } catch (error) { - logger.error('Failed to update fleet position', { - correlationId: req.correlationId, - playerId: req.user?.id, - fleetId: req.params.fleetId, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError) { - return res.status(400).json({ - error: error.message, - code: 'VALIDATION_ERROR', - }); - } - - if (error instanceof NotFoundError) { - return res.status(404).json({ - error: error.message, - code: 'NOT_FOUND', - }); - } - - next(error); - } - } - - /** - * Get available combat types and configurations - * GET /api/combat/types - */ - async getCombatTypes(req, res, next) { - try { - const correlationId = req.correlationId; - - logger.info('Combat types request', { correlationId }); - - if (!this.combatService) { - await this.initialize(); - } - - const combatTypes = await this.combatService.getAvailableCombatTypes(correlationId); - - logger.info('Combat types retrieved', { - correlationId, - count: combatTypes.length, - }); - - res.json({ - success: true, - data: combatTypes, - }); - - } catch (error) { - logger.error('Failed to get combat types', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } - - /** - * Force resolve a combat (admin only) - * POST /api/combat/resolve/:battleId - */ - async forceResolveCombat(req, res, next) { - try { - const correlationId = req.correlationId; - const battleId = parseInt(req.params.battleId); - - if (!battleId || isNaN(battleId)) { - return res.status(400).json({ - error: 'Valid battle ID is required', - code: 'INVALID_BATTLE_ID', - }); - } - - logger.info('Force resolve combat request', { - correlationId, - battleId, - adminUser: req.user?.id, - }); - - if (!this.combatService) { - await this.initialize(); - } - - const result = await this.combatService.processCombat(battleId, correlationId); - - logger.info('Combat force resolved', { - correlationId, - battleId, - outcome: result.outcome, - }); - - res.json({ - success: true, - data: result, - message: 'Combat resolved successfully', - }); - - } catch (error) { - logger.error('Failed to force resolve combat', { - correlationId: req.correlationId, - battleId: req.params.battleId, - error: error.message, - stack: error.stack, - }); - - if (error instanceof NotFoundError) { - return res.status(404).json({ - error: error.message, - code: 'NOT_FOUND', - }); - } - - if (error instanceof ConflictError) { - return res.status(409).json({ - error: error.message, - code: 'CONFLICT_ERROR', - }); - } - - next(error); - } - } - - /** - * Get combat queue status (admin only) - * GET /api/combat/queue - */ - async getCombatQueue(req, res, next) { - try { - const correlationId = req.correlationId; - const status = req.query.status || null; - const limit = parseInt(req.query.limit) || 50; - - logger.info('Combat queue request', { - correlationId, - status, - limit, - adminUser: req.user?.id, - }); - - if (!this.combatService) { - await this.initialize(); - } - - const queue = await this.combatService.getCombatQueue({ status, limit }, correlationId); - - logger.info('Combat queue retrieved', { - correlationId, - count: queue.length, - }); - - res.json({ - success: true, - data: queue, - }); - - } catch (error) { - logger.error('Failed to get combat queue', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); - - next(error); - } - } -} - -// Export singleton instance -const combatController = new CombatController(); - -module.exports = { - CombatController, - - // Export bound methods for route usage - initiateCombat: combatController.initiateCombat.bind(combatController), - getActiveCombats: combatController.getActiveCombats.bind(combatController), - getCombatHistory: combatController.getCombatHistory.bind(combatController), - getCombatEncounter: combatController.getCombatEncounter.bind(combatController), - getCombatStatistics: combatController.getCombatStatistics.bind(combatController), - updateFleetPosition: combatController.updateFleetPosition.bind(combatController), - getCombatTypes: combatController.getCombatTypes.bind(combatController), - forceResolveCombat: combatController.forceResolveCombat.bind(combatController), - getCombatQueue: combatController.getCombatQueue.bind(combatController), -}; diff --git a/src/controllers/api/fleet.controller.js b/src/controllers/api/fleet.controller.js deleted file mode 100644 index c083f37..0000000 --- a/src/controllers/api/fleet.controller.js +++ /dev/null @@ -1,555 +0,0 @@ -/** - * Fleet API Controller - * Handles fleet management REST API endpoints - */ - -const logger = require('../../utils/logger'); -const serviceLocator = require('../../services/ServiceLocator'); -const { - validateCreateFleet, - validateMoveFleet, - validateFleetId, - validateDesignId, - validateShipDesignQuery, - validatePagination, - customValidations -} = require('../../validators/fleet.validators'); - -class FleetController { - constructor() { - this.fleetService = null; - this.shipDesignService = null; - } - - /** - * Initialize services - */ - initializeServices() { - if (!this.fleetService) { - this.fleetService = serviceLocator.get('fleetService'); - } - if (!this.shipDesignService) { - this.shipDesignService = serviceLocator.get('shipDesignService'); - } - - if (!this.fleetService || !this.shipDesignService) { - throw new Error('Fleet services not properly registered in ServiceLocator'); - } - } - - /** - * Get all fleets for the authenticated player - * GET /api/fleets - */ - async getPlayerFleets(req, res, next) { - try { - this.initializeServices(); - - const playerId = req.user.id; - const correlationId = req.correlationId; - - logger.info('Getting player fleets', { - correlationId, - playerId, - endpoint: 'GET /api/fleets' - }); - - const fleets = await this.fleetService.getPlayerFleets(playerId, correlationId); - - res.json({ - success: true, - data: { - fleets: fleets, - total_fleets: fleets.length, - total_ships: fleets.reduce((sum, fleet) => sum + (fleet.total_ships || 0), 0) - }, - timestamp: new Date().toISOString() - }); - - } catch (error) { - logger.error('Failed to get player fleets', { - correlationId: req.correlationId, - playerId: req.user?.id, - error: error.message, - stack: error.stack - }); - next(error); - } - } - - /** - * Get fleet details by ID - * GET /api/fleets/:fleetId - */ - async getFleetDetails(req, res, next) { - try { - this.initializeServices(); - - const playerId = req.user.id; - const fleetId = parseInt(req.params.fleetId); - const correlationId = req.correlationId; - - logger.info('Getting fleet details', { - correlationId, - playerId, - fleetId, - endpoint: 'GET /api/fleets/:fleetId' - }); - - // Validate fleet ownership - const ownsFleet = await customValidations.validateFleetOwnership(fleetId, playerId); - if (!ownsFleet) { - return res.status(404).json({ - success: false, - error: 'Fleet not found', - message: 'The specified fleet does not exist or you do not have access to it' - }); - } - - const fleet = await this.fleetService.getFleetDetails(fleetId, playerId, correlationId); - - res.json({ - success: true, - data: fleet, - timestamp: new Date().toISOString() - }); - - } catch (error) { - logger.error('Failed to get fleet details', { - correlationId: req.correlationId, - playerId: req.user?.id, - fleetId: req.params.fleetId, - error: error.message, - stack: error.stack - }); - next(error); - } - } - - /** - * Create a new fleet - * POST /api/fleets - */ - async createFleet(req, res, next) { - try { - this.initializeServices(); - - const playerId = req.user.id; - const fleetData = req.body; - const correlationId = req.correlationId; - - logger.info('Creating new fleet', { - correlationId, - playerId, - fleetName: fleetData.name, - location: fleetData.location, - endpoint: 'POST /api/fleets' - }); - - // Validate colony ownership - const ownsColony = await customValidations.validateColonyOwnership(fleetData.location, playerId); - if (!ownsColony) { - return res.status(400).json({ - success: false, - error: 'Invalid location', - message: 'You can only create fleets at your own colonies' - }); - } - - const result = await this.fleetService.createFleet(playerId, fleetData, correlationId); - - res.status(201).json({ - success: true, - data: result, - message: 'Fleet created successfully', - timestamp: new Date().toISOString() - }); - - } catch (error) { - logger.error('Failed to create fleet', { - correlationId: req.correlationId, - playerId: req.user?.id, - fleetData: req.body, - error: error.message, - stack: error.stack - }); - - // Handle specific error types - if (error.statusCode === 400) { - return res.status(400).json({ - success: false, - error: error.message, - details: error.details, - message: 'Fleet creation failed due to validation errors' - }); - } - - next(error); - } - } - - /** - * Move a fleet to a new location - * POST /api/fleets/:fleetId/move - */ - async moveFleet(req, res, next) { - try { - this.initializeServices(); - - const playerId = req.user.id; - const fleetId = parseInt(req.params.fleetId); - const { destination } = req.body; - const correlationId = req.correlationId; - - logger.info('Moving fleet', { - correlationId, - playerId, - fleetId, - destination, - endpoint: 'POST /api/fleets/:fleetId/move' - }); - - // Validate fleet ownership - const ownsFleet = await customValidations.validateFleetOwnership(fleetId, playerId); - if (!ownsFleet) { - return res.status(404).json({ - success: false, - error: 'Fleet not found', - message: 'The specified fleet does not exist or you do not have access to it' - }); - } - - // Validate fleet can move - const canMove = await customValidations.validateFleetAction(fleetId, 'idle'); - if (!canMove) { - return res.status(400).json({ - success: false, - error: 'Fleet cannot move', - message: 'Fleet must be idle to initiate movement' - }); - } - - const result = await this.fleetService.moveFleet(fleetId, playerId, destination, correlationId); - - res.json({ - success: true, - data: result, - message: 'Fleet movement initiated successfully', - timestamp: new Date().toISOString() - }); - - } catch (error) { - logger.error('Failed to move fleet', { - correlationId: req.correlationId, - playerId: req.user?.id, - fleetId: req.params.fleetId, - destination: req.body.destination, - error: error.message, - stack: error.stack - }); - - if (error.statusCode === 400 || error.statusCode === 404) { - return res.status(error.statusCode).json({ - success: false, - error: error.message, - message: 'Fleet movement failed' - }); - } - - next(error); - } - } - - /** - * Disband a fleet - * DELETE /api/fleets/:fleetId - */ - async disbandFleet(req, res, next) { - try { - this.initializeServices(); - - const playerId = req.user.id; - const fleetId = parseInt(req.params.fleetId); - const correlationId = req.correlationId; - - logger.info('Disbanding fleet', { - correlationId, - playerId, - fleetId, - endpoint: 'DELETE /api/fleets/:fleetId' - }); - - // Validate fleet ownership - const ownsFleet = await customValidations.validateFleetOwnership(fleetId, playerId); - if (!ownsFleet) { - return res.status(404).json({ - success: false, - error: 'Fleet not found', - message: 'The specified fleet does not exist or you do not have access to it' - }); - } - - // Validate fleet can be disbanded - const canDisband = await customValidations.validateFleetAction(fleetId, ['idle', 'moving', 'constructing']); - if (!canDisband) { - return res.status(400).json({ - success: false, - error: 'Fleet cannot be disbanded', - message: 'Fleet cannot be disbanded while in combat' - }); - } - - const result = await this.fleetService.disbandFleet(fleetId, playerId, correlationId); - - res.json({ - success: true, - data: result, - message: 'Fleet disbanded successfully', - timestamp: new Date().toISOString() - }); - - } catch (error) { - logger.error('Failed to disband fleet', { - correlationId: req.correlationId, - playerId: req.user?.id, - fleetId: req.params.fleetId, - error: error.message, - stack: error.stack - }); - - if (error.statusCode === 400 || error.statusCode === 404) { - return res.status(error.statusCode).json({ - success: false, - error: error.message, - message: 'Fleet disbanding failed' - }); - } - - next(error); - } - } - - /** - * Get available ship designs for the player - * GET /api/fleets/ship-designs - */ - async getAvailableShipDesigns(req, res, next) { - try { - this.initializeServices(); - - const playerId = req.user.id; - const correlationId = req.correlationId; - const { ship_class, tier, available_only } = req.query; - - logger.info('Getting available ship designs', { - correlationId, - playerId, - filters: { ship_class, tier, available_only }, - endpoint: 'GET /api/fleets/ship-designs' - }); - - let designs; - - if (ship_class) { - designs = await this.shipDesignService.getDesignsByClass(playerId, ship_class, correlationId); - } else { - designs = await this.shipDesignService.getAvailableDesigns(playerId, correlationId); - } - - // Apply tier filter if specified - if (tier) { - const tierNum = parseInt(tier); - designs = designs.filter(design => design.tier === tierNum); - } - - // Filter by availability if requested - if (available_only === false || available_only === 'false') { - // Include all designs regardless of availability - } else { - // Only include available designs (default behavior) - designs = designs.filter(design => design.is_available !== false); - } - - res.json({ - success: true, - data: { - ship_designs: designs, - total_designs: designs.length, - filters_applied: { - ship_class: ship_class || null, - tier: tier ? parseInt(tier) : null, - available_only: available_only !== false - } - }, - timestamp: new Date().toISOString() - }); - - } catch (error) { - logger.error('Failed to get available ship designs', { - correlationId: req.correlationId, - playerId: req.user?.id, - error: error.message, - stack: error.stack - }); - next(error); - } - } - - /** - * Get ship design details - * GET /api/fleets/ship-designs/:designId - */ - async getShipDesignDetails(req, res, next) { - try { - this.initializeServices(); - - const playerId = req.user.id; - const designId = parseInt(req.params.designId); - const correlationId = req.correlationId; - - logger.info('Getting ship design details', { - correlationId, - playerId, - designId, - endpoint: 'GET /api/fleets/ship-designs/:designId' - }); - - const design = await this.shipDesignService.getDesignDetails(designId, playerId, correlationId); - - res.json({ - success: true, - data: design, - timestamp: new Date().toISOString() - }); - - } catch (error) { - logger.error('Failed to get ship design details', { - correlationId: req.correlationId, - playerId: req.user?.id, - designId: req.params.designId, - error: error.message, - stack: error.stack - }); - - if (error.statusCode === 404) { - return res.status(404).json({ - success: false, - error: 'Ship design not found', - message: 'The specified ship design does not exist or is not available to you' - }); - } - - next(error); - } - } - - /** - * Get ship classes information - * GET /api/fleets/ship-classes - */ - async getShipClassesInfo(req, res, next) { - try { - this.initializeServices(); - - const correlationId = req.correlationId; - - logger.info('Getting ship classes information', { - correlationId, - endpoint: 'GET /api/fleets/ship-classes' - }); - - const info = this.shipDesignService.getShipClassesInfo(); - - res.json({ - success: true, - data: info, - timestamp: new Date().toISOString() - }); - - } catch (error) { - logger.error('Failed to get ship classes information', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack - }); - next(error); - } - } - - /** - * Validate ship construction possibility - * POST /api/fleets/validate-construction - */ - async validateShipConstruction(req, res, next) { - try { - this.initializeServices(); - - const playerId = req.user.id; - const { design_id, quantity = 1 } = req.body; - const correlationId = req.correlationId; - - logger.info('Validating ship construction', { - correlationId, - playerId, - designId: design_id, - quantity, - endpoint: 'POST /api/fleets/validate-construction' - }); - - if (!design_id || !Number.isInteger(design_id) || design_id < 1) { - return res.status(400).json({ - success: false, - error: 'Invalid design ID', - message: 'Design ID must be a positive integer' - }); - } - - if (!Number.isInteger(quantity) || quantity < 1 || quantity > 100) { - return res.status(400).json({ - success: false, - error: 'Invalid quantity', - message: 'Quantity must be between 1 and 100' - }); - } - - const validation = await this.shipDesignService.validateShipConstruction( - playerId, - design_id, - quantity, - correlationId - ); - - res.json({ - success: true, - data: validation, - timestamp: new Date().toISOString() - }); - - } catch (error) { - logger.error('Failed to validate ship construction', { - correlationId: req.correlationId, - playerId: req.user?.id, - requestBody: req.body, - error: error.message, - stack: error.stack - }); - next(error); - } - } -} - -// Create controller instance -const fleetController = new FleetController(); - -// Export controller methods with proper binding -module.exports = { - getPlayerFleets: [validatePagination, fleetController.getPlayerFleets.bind(fleetController)], - getFleetDetails: [validateFleetId, fleetController.getFleetDetails.bind(fleetController)], - createFleet: [validateCreateFleet, fleetController.createFleet.bind(fleetController)], - moveFleet: [validateFleetId, validateMoveFleet, fleetController.moveFleet.bind(fleetController)], - disbandFleet: [validateFleetId, fleetController.disbandFleet.bind(fleetController)], - getAvailableShipDesigns: [validateShipDesignQuery, fleetController.getAvailableShipDesigns.bind(fleetController)], - getShipDesignDetails: [validateDesignId, fleetController.getShipDesignDetails.bind(fleetController)], - getShipClassesInfo: fleetController.getShipClassesInfo.bind(fleetController), - validateShipConstruction: fleetController.validateShipConstruction.bind(fleetController) -}; \ No newline at end of file diff --git a/src/controllers/api/player.controller.js b/src/controllers/api/player.controller.js index a900645..06f5547 100644 --- a/src/controllers/api/player.controller.js +++ b/src/controllers/api/player.controller.js @@ -14,55 +14,55 @@ const playerService = new PlayerService(); * GET /api/player/dashboard */ const getDashboard = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; + const correlationId = req.correlationId; + const playerId = req.user.playerId; - logger.info('Player dashboard request received', { - correlationId, - playerId, - }); + logger.info('Player dashboard request received', { + correlationId, + playerId + }); - // Get player profile with resources and stats - const profile = await playerService.getPlayerProfile(playerId, correlationId); + // Get player profile with resources and stats + const profile = await playerService.getPlayerProfile(playerId, correlationId); - // TODO: Add additional dashboard data such as: - // - Recent activities - // - Colony summaries - // - Fleet statuses - // - Research progress - // - Messages/notifications + // TODO: Add additional dashboard data such as: + // - Recent activities + // - Colony summaries + // - Fleet statuses + // - Research progress + // - Messages/notifications - const dashboardData = { - player: profile, - summary: { - totalColonies: profile.stats.coloniesCount, - totalFleets: profile.stats.fleetsCount, - totalBattles: profile.stats.totalBattles, - winRate: profile.stats.totalBattles > 0 - ? Math.round((profile.stats.battlesWon / profile.stats.totalBattles) * 100) - : 0, - }, - // Placeholder for future dashboard sections - recentActivity: [], - notifications: [], - gameStatus: { - online: true, - lastTick: new Date().toISOString(), - }, - }; + const dashboardData = { + player: profile, + summary: { + totalColonies: profile.stats.coloniesCount, + totalFleets: profile.stats.fleetsCount, + totalBattles: profile.stats.totalBattles, + winRate: profile.stats.totalBattles > 0 + ? Math.round((profile.stats.battlesWon / profile.stats.totalBattles) * 100) + : 0 + }, + // Placeholder for future dashboard sections + recentActivity: [], + notifications: [], + gameStatus: { + online: true, + lastTick: new Date().toISOString() + } + }; - logger.info('Player dashboard data retrieved', { - correlationId, - playerId, - username: profile.username, - }); + logger.info('Player dashboard data retrieved', { + correlationId, + playerId, + username: profile.username + }); - res.status(200).json({ - success: true, - message: 'Dashboard data retrieved successfully', - data: dashboardData, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Dashboard data retrieved successfully', + data: dashboardData, + correlationId + }); }); /** @@ -70,32 +70,32 @@ const getDashboard = asyncHandler(async (req, res) => { * GET /api/player/resources */ const getResources = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; + const correlationId = req.correlationId; + const playerId = req.user.playerId; - logger.info('Player resources request received', { - correlationId, - playerId, - }); + logger.info('Player resources request received', { + correlationId, + playerId + }); - const profile = await playerService.getPlayerProfile(playerId, correlationId); + const profile = await playerService.getPlayerProfile(playerId, correlationId); - logger.info('Player resources retrieved', { - correlationId, - playerId, - scrap: profile.resources.scrap, - energy: profile.resources.energy, - }); + logger.info('Player resources retrieved', { + correlationId, + playerId, + scrap: profile.resources.scrap, + energy: profile.resources.energy + }); - res.status(200).json({ - success: true, - message: 'Resources retrieved successfully', - data: { - resources: profile.resources, - lastUpdated: new Date().toISOString(), - }, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Resources retrieved successfully', + data: { + resources: profile.resources, + lastUpdated: new Date().toISOString() + }, + correlationId + }); }); /** @@ -103,43 +103,43 @@ const getResources = asyncHandler(async (req, res) => { * GET /api/player/stats */ const getStats = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; + const correlationId = req.correlationId; + const playerId = req.user.playerId; - logger.info('Player statistics request received', { - correlationId, - playerId, - }); + logger.info('Player statistics request received', { + correlationId, + playerId + }); - const profile = await playerService.getPlayerProfile(playerId, correlationId); + const profile = await playerService.getPlayerProfile(playerId, correlationId); - const detailedStats = { - ...profile.stats, - winRate: profile.stats.totalBattles > 0 - ? Math.round((profile.stats.battlesWon / profile.stats.totalBattles) * 100) - : 0, - lossRate: profile.stats.totalBattles > 0 - ? Math.round(((profile.stats.totalBattles - profile.stats.battlesWon) / profile.stats.totalBattles) * 100) - : 0, - accountAge: Math.floor((Date.now() - new Date(profile.createdAt).getTime()) / (1000 * 60 * 60 * 24)), // days - }; + const detailedStats = { + ...profile.stats, + winRate: profile.stats.totalBattles > 0 + ? Math.round((profile.stats.battlesWon / profile.stats.totalBattles) * 100) + : 0, + lossRate: profile.stats.totalBattles > 0 + ? Math.round(((profile.stats.totalBattles - profile.stats.battlesWon) / profile.stats.totalBattles) * 100) + : 0, + accountAge: Math.floor((Date.now() - new Date(profile.createdAt).getTime()) / (1000 * 60 * 60 * 24)) // days + }; - logger.info('Player statistics retrieved', { - correlationId, - playerId, - totalBattles: detailedStats.totalBattles, - winRate: detailedStats.winRate, - }); + logger.info('Player statistics retrieved', { + correlationId, + playerId, + totalBattles: detailedStats.totalBattles, + winRate: detailedStats.winRate + }); - res.status(200).json({ - success: true, - message: 'Statistics retrieved successfully', - data: { - stats: detailedStats, - lastUpdated: new Date().toISOString(), - }, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Statistics retrieved successfully', + data: { + stats: detailedStats, + lastUpdated: new Date().toISOString() + }, + correlationId + }); }); /** @@ -147,32 +147,32 @@ const getStats = asyncHandler(async (req, res) => { * PUT /api/player/settings */ const updateSettings = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const settings = req.body; + const correlationId = req.correlationId; + const playerId = req.user.playerId; + const settings = req.body; - logger.info('Player settings update request received', { - correlationId, - playerId, - settingsKeys: Object.keys(settings), - }); + logger.info('Player settings update request received', { + correlationId, + playerId, + settingsKeys: Object.keys(settings) + }); - // TODO: Implement player settings update - // This would involve: - // 1. Validate settings data - // 2. Update player_settings table - // 3. Return updated settings + // TODO: Implement player settings update + // This would involve: + // 1. Validate settings data + // 2. Update player_settings table + // 3. Return updated settings - logger.warn('Player settings update requested but not implemented', { - correlationId, - playerId, - }); + logger.warn('Player settings update requested but not implemented', { + correlationId, + playerId + }); - res.status(501).json({ - success: false, - message: 'Player settings update feature not yet implemented', - correlationId, - }); + res.status(501).json({ + success: false, + message: 'Player settings update feature not yet implemented', + correlationId + }); }); /** @@ -180,49 +180,49 @@ const updateSettings = asyncHandler(async (req, res) => { * GET /api/player/activity */ const getActivity = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const { page = 1, limit = 20 } = req.query; + const correlationId = req.correlationId; + const playerId = req.user.playerId; + const { page = 1, limit = 20 } = req.query; - logger.info('Player activity log request received', { - correlationId, - playerId, - page, - limit, - }); + logger.info('Player activity log request received', { + correlationId, + playerId, + page, + limit + }); - // TODO: Implement player activity log retrieval - // This would show recent actions like: - // - Colony creations/updates - // - Fleet movements - // - Research completions - // - Battle results - // - Resource transactions + // TODO: Implement player activity log retrieval + // This would show recent actions like: + // - Colony creations/updates + // - Fleet movements + // - Research completions + // - Battle results + // - Resource transactions - const mockActivity = { - activities: [], - pagination: { - page: parseInt(page), - limit: parseInt(limit), - total: 0, - totalPages: 0, - hasNext: false, - hasPrev: false, - }, - }; + const mockActivity = { + activities: [], + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total: 0, + totalPages: 0, + hasNext: false, + hasPrev: false + } + }; - logger.info('Player activity log retrieved', { - correlationId, - playerId, - activitiesCount: mockActivity.activities.length, - }); + logger.info('Player activity log retrieved', { + correlationId, + playerId, + activitiesCount: mockActivity.activities.length + }); - res.status(200).json({ - success: true, - message: 'Activity log retrieved successfully', - data: mockActivity, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Activity log retrieved successfully', + data: mockActivity, + correlationId + }); }); /** @@ -230,42 +230,42 @@ const getActivity = asyncHandler(async (req, res) => { * GET /api/player/notifications */ const getNotifications = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const { unreadOnly = false } = req.query; + const correlationId = req.correlationId; + const playerId = req.user.playerId; + const { unreadOnly = false } = req.query; - logger.info('Player notifications request received', { - correlationId, - playerId, - unreadOnly, - }); + logger.info('Player notifications request received', { + correlationId, + playerId, + unreadOnly + }); - // TODO: Implement player notifications retrieval - // This would show: - // - System messages - // - Battle results - // - Research completions - // - Fleet arrival notifications - // - Player messages + // TODO: Implement player notifications retrieval + // This would show: + // - System messages + // - Battle results + // - Research completions + // - Fleet arrival notifications + // - Player messages - const mockNotifications = { - notifications: [], - unreadCount: 0, - totalCount: 0, - }; + const mockNotifications = { + notifications: [], + unreadCount: 0, + totalCount: 0 + }; - logger.info('Player notifications retrieved', { - correlationId, - playerId, - unreadCount: mockNotifications.unreadCount, - }); + logger.info('Player notifications retrieved', { + correlationId, + playerId, + unreadCount: mockNotifications.unreadCount + }); - res.status(200).json({ - success: true, - message: 'Notifications retrieved successfully', - data: mockNotifications, - correlationId, - }); + res.status(200).json({ + success: true, + message: 'Notifications retrieved successfully', + data: mockNotifications, + correlationId + }); }); /** @@ -273,35 +273,35 @@ const getNotifications = asyncHandler(async (req, res) => { * PUT /api/player/notifications/read */ const markNotificationsRead = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const { notificationIds } = req.body; + const correlationId = req.correlationId; + const playerId = req.user.playerId; + const { notificationIds } = req.body; - logger.info('Mark notifications read request received', { - correlationId, - playerId, - notificationCount: notificationIds?.length || 0, - }); + logger.info('Mark notifications read request received', { + correlationId, + playerId, + notificationCount: notificationIds?.length || 0 + }); - // TODO: Implement notification marking as read - logger.warn('Mark notifications read requested but not implemented', { - correlationId, - playerId, - }); + // TODO: Implement notification marking as read + logger.warn('Mark notifications read requested but not implemented', { + correlationId, + playerId + }); - res.status(501).json({ - success: false, - message: 'Mark notifications read feature not yet implemented', - correlationId, - }); + res.status(501).json({ + success: false, + message: 'Mark notifications read feature not yet implemented', + correlationId + }); }); module.exports = { - getDashboard, - getResources, - getStats, - updateSettings, - getActivity, - getNotifications, - markNotificationsRead, -}; + getDashboard, + getResources, + getStats, + updateSettings, + getActivity, + getNotifications, + markNotificationsRead +}; \ No newline at end of file diff --git a/src/controllers/api/research.controller.js b/src/controllers/api/research.controller.js deleted file mode 100644 index e25649f..0000000 --- a/src/controllers/api/research.controller.js +++ /dev/null @@ -1,495 +0,0 @@ -/** - * Research API Controller - * Handles HTTP requests for research and technology management - */ - -const logger = require('../../utils/logger'); -const ResearchService = require('../../services/research/ResearchService'); -const ServiceLocator = require('../../services/ServiceLocator'); - -class ResearchController { - constructor() { - this.researchService = null; - } - - /** - * Initialize controller with services - */ - initialize() { - const gameEventService = ServiceLocator.get('gameEventService'); - this.researchService = new ResearchService(gameEventService); - } - - /** - * Get available technologies for the authenticated player - * GET /api/research/available - */ - async getAvailableTechnologies(req, res) { - const correlationId = req.correlationId; - const playerId = req.user.id; - - try { - logger.info('API request: Get available technologies', { - correlationId, - playerId, - endpoint: '/api/research/available' - }); - - if (!this.researchService) { - this.initialize(); - } - - const technologies = await this.researchService.getAvailableTechnologies( - playerId, - correlationId - ); - - res.json({ - success: true, - data: { - technologies, - count: technologies.length - }, - correlationId - }); - - } catch (error) { - logger.error('Failed to get available technologies', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - - const statusCode = error.statusCode || 500; - res.status(statusCode).json({ - success: false, - error: error.message, - details: error.details || null, - correlationId - }); - } - } - - /** - * Get current research status for the authenticated player - * GET /api/research/status - */ - async getResearchStatus(req, res) { - const correlationId = req.correlationId; - const playerId = req.user.id; - - try { - logger.info('API request: Get research status', { - correlationId, - playerId, - endpoint: '/api/research/status' - }); - - if (!this.researchService) { - this.initialize(); - } - - const status = await this.researchService.getResearchStatus( - playerId, - correlationId - ); - - res.json({ - success: true, - data: status, - correlationId - }); - - } catch (error) { - logger.error('Failed to get research status', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - - const statusCode = error.statusCode || 500; - res.status(statusCode).json({ - success: false, - error: error.message, - details: error.details || null, - correlationId - }); - } - } - - /** - * Start research on a technology - * POST /api/research/start - * Body: { technology_id: number } - */ - async startResearch(req, res) { - const correlationId = req.correlationId; - const playerId = req.user.id; - const { technology_id } = req.body; - - try { - logger.info('API request: Start research', { - correlationId, - playerId, - technologyId: technology_id, - endpoint: '/api/research/start' - }); - - // Validate input - if (!technology_id || !Number.isInteger(technology_id)) { - return res.status(400).json({ - success: false, - error: 'Valid technology_id is required', - correlationId - }); - } - - if (!this.researchService) { - this.initialize(); - } - - const result = await this.researchService.startResearch( - playerId, - technology_id, - correlationId - ); - - res.status(201).json({ - success: true, - data: result, - message: 'Research started successfully', - correlationId - }); - - } catch (error) { - logger.error('Failed to start research', { - correlationId, - playerId, - technologyId: technology_id, - error: error.message, - stack: error.stack - }); - - const statusCode = error.statusCode || 500; - res.status(statusCode).json({ - success: false, - error: error.message, - details: error.details || null, - correlationId - }); - } - } - - /** - * Cancel current research - * POST /api/research/cancel - */ - async cancelResearch(req, res) { - const correlationId = req.correlationId; - const playerId = req.user.id; - - try { - logger.info('API request: Cancel research', { - correlationId, - playerId, - endpoint: '/api/research/cancel' - }); - - if (!this.researchService) { - this.initialize(); - } - - const result = await this.researchService.cancelResearch( - playerId, - correlationId - ); - - res.json({ - success: true, - data: result, - message: 'Research cancelled successfully', - correlationId - }); - - } catch (error) { - logger.error('Failed to cancel research', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - - const statusCode = error.statusCode || 500; - res.status(statusCode).json({ - success: false, - error: error.message, - details: error.details || null, - correlationId - }); - } - } - - /** - * Get completed technologies for the authenticated player - * GET /api/research/completed - */ - async getCompletedTechnologies(req, res) { - const correlationId = req.correlationId; - const playerId = req.user.id; - - try { - logger.info('API request: Get completed technologies', { - correlationId, - playerId, - endpoint: '/api/research/completed' - }); - - if (!this.researchService) { - this.initialize(); - } - - const technologies = await this.researchService.getCompletedTechnologies( - playerId, - correlationId - ); - - res.json({ - success: true, - data: { - technologies, - count: technologies.length - }, - correlationId - }); - - } catch (error) { - logger.error('Failed to get completed technologies', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - - const statusCode = error.statusCode || 500; - res.status(statusCode).json({ - success: false, - error: error.message, - details: error.details || null, - correlationId - }); - } - } - - /** - * Get technology tree (all technologies with their relationships) - * GET /api/research/technology-tree - */ - async getTechnologyTree(req, res) { - const correlationId = req.correlationId; - const playerId = req.user.id; - - try { - logger.info('API request: Get technology tree', { - correlationId, - playerId, - endpoint: '/api/research/technology-tree' - }); - - const { TECHNOLOGIES, TECH_CATEGORIES } = require('../../data/technologies'); - - // Get player's research progress - if (!this.researchService) { - this.initialize(); - } - - const [availableTechs, completedTechs] = await Promise.all([ - this.researchService.getAvailableTechnologies(playerId, correlationId), - this.researchService.getCompletedTechnologies(playerId, correlationId) - ]); - - // Create status maps - const availableMap = new Map(); - availableTechs.forEach(tech => { - availableMap.set(tech.id, tech.research_status); - }); - - const completedMap = new Map(); - completedTechs.forEach(tech => { - completedMap.set(tech.id, true); - }); - - // Build technology tree with status information - const technologyTree = TECHNOLOGIES.map(tech => { - let status = 'unavailable'; - let progress = 0; - let started_at = null; - - if (completedMap.has(tech.id)) { - status = 'completed'; - } else if (availableMap.has(tech.id)) { - status = availableMap.get(tech.id); - const availableTech = availableTechs.find(t => t.id === tech.id); - if (availableTech) { - progress = availableTech.progress || 0; - started_at = availableTech.started_at; - } - } - - return { - ...tech, - status, - progress, - started_at, - completion_percentage: tech.research_time > 0 ? - (progress / tech.research_time) * 100 : 0 - }; - }); - - // Group by category and tier for easier frontend handling - const categories = {}; - Object.values(TECH_CATEGORIES).forEach(category => { - categories[category] = {}; - for (let tier = 1; tier <= 5; tier++) { - categories[category][tier] = technologyTree.filter( - tech => tech.category === category && tech.tier === tier - ); - } - }); - - res.json({ - success: true, - data: { - technology_tree: technologyTree, - categories: categories, - tech_categories: TECH_CATEGORIES, - player_stats: { - completed_count: completedTechs.length, - available_count: availableTechs.filter(t => t.research_status === 'available').length, - researching_count: availableTechs.filter(t => t.research_status === 'researching').length - } - }, - correlationId - }); - - } catch (error) { - logger.error('Failed to get technology tree', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - - const statusCode = error.statusCode || 500; - res.status(statusCode).json({ - success: false, - error: error.message, - details: error.details || null, - correlationId - }); - } - } - - /** - * Get research queue (current and queued research) - * GET /api/research/queue - */ - async getResearchQueue(req, res) { - const correlationId = req.correlationId; - const playerId = req.user.id; - - try { - logger.info('API request: Get research queue', { - correlationId, - playerId, - endpoint: '/api/research/queue' - }); - - if (!this.researchService) { - this.initialize(); - } - - // For now, we only support one research at a time - // This endpoint returns current research and could be extended for queue functionality - const status = await this.researchService.getResearchStatus( - playerId, - correlationId - ); - - const queue = []; - if (status.current_research) { - queue.push({ - position: 1, - ...status.current_research, - estimated_completion: this.calculateEstimatedCompletion( - status.current_research, - status.bonuses - ) - }); - } - - res.json({ - success: true, - data: { - queue, - queue_length: queue.length, - max_queue_length: 1, // Current limitation - current_research: status.current_research, - research_bonuses: status.bonuses - }, - correlationId - }); - - } catch (error) { - logger.error('Failed to get research queue', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - - const statusCode = error.statusCode || 500; - res.status(statusCode).json({ - success: false, - error: error.message, - details: error.details || null, - correlationId - }); - } - } - - /** - * Helper method to calculate estimated completion time - * @param {Object} research - Current research data - * @param {Object} bonuses - Research bonuses - * @returns {string} Estimated completion time - */ - calculateEstimatedCompletion(research, bonuses) { - if (!research || !research.started_at) { - return null; - } - - const totalSpeedMultiplier = 1.0 + (bonuses.research_speed_bonus || 0); - const remainingTime = Math.max(0, research.research_time - research.progress); - const adjustedRemainingTime = remainingTime / totalSpeedMultiplier; - - const startedAt = new Date(research.started_at); - const estimatedCompletion = new Date(startedAt.getTime() + (adjustedRemainingTime * 60 * 1000)); - - return estimatedCompletion.toISOString(); - } -} - -// Create controller instance -const researchController = new ResearchController(); - -module.exports = { - getAvailableTechnologies: (req, res) => researchController.getAvailableTechnologies(req, res), - getResearchStatus: (req, res) => researchController.getResearchStatus(req, res), - startResearch: (req, res) => researchController.startResearch(req, res), - cancelResearch: (req, res) => researchController.cancelResearch(req, res), - getCompletedTechnologies: (req, res) => researchController.getCompletedTechnologies(req, res), - getTechnologyTree: (req, res) => researchController.getTechnologyTree(req, res), - getResearchQueue: (req, res) => researchController.getResearchQueue(req, res) -}; \ No newline at end of file diff --git a/src/controllers/player/colony.controller.js b/src/controllers/player/colony.controller.js deleted file mode 100644 index 1791299..0000000 --- a/src/controllers/player/colony.controller.js +++ /dev/null @@ -1,315 +0,0 @@ -/** - * Colony Controller - * Handles colony-related API endpoints for players - */ - -const ColonyService = require('../../services/galaxy/ColonyService'); -const { asyncHandler } = require('../../middleware/error.middleware'); -const logger = require('../../utils/logger'); -const serviceLocator = require('../../services/ServiceLocator'); - -// Create colony service with WebSocket integration -function getColonyService() { - const gameEventService = serviceLocator.get('gameEventService'); - return new ColonyService(gameEventService); -} - -/** - * Create a new colony - * POST /api/player/colonies - */ -const createColony = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const { name, coordinates, planet_type_id } = req.body; - - logger.info('Colony creation request received', { - correlationId, - playerId, - name, - coordinates, - planet_type_id, - }); - - const colonyService = getColonyService(); - const colony = await colonyService.createColony(playerId, { - name, - coordinates, - planet_type_id, - }, correlationId); - - logger.info('Colony created successfully', { - correlationId, - playerId, - colonyId: colony.id, - name: colony.name, - coordinates: colony.coordinates, - }); - - res.status(201).json({ - success: true, - message: 'Colony created successfully', - data: { - colony, - }, - correlationId, - }); -}); - -/** - * Get all colonies owned by the player - * GET /api/player/colonies - */ -const getPlayerColonies = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - - logger.info('Player colonies request received', { - correlationId, - playerId, - }); - - const colonyService = getColonyService(); - const colonies = await colonyService.getPlayerColonies(playerId, correlationId); - - logger.info('Player colonies retrieved', { - correlationId, - playerId, - colonyCount: colonies.length, - }); - - res.status(200).json({ - success: true, - message: 'Colonies retrieved successfully', - data: { - colonies, - count: colonies.length, - }, - correlationId, - }); -}); - -/** - * Get detailed information about a specific colony - * GET /api/player/colonies/:colonyId - */ -const getColonyDetails = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const colonyId = parseInt(req.params.colonyId); - - logger.info('Colony details request received', { - correlationId, - playerId, - colonyId, - }); - - // Verify colony ownership through the service - const colonyService = getColonyService(); - const colony = await colonyService.getColonyDetails(colonyId, correlationId); - - // Additional ownership check - if (colony.player_id !== playerId) { - logger.warn('Unauthorized colony access attempt', { - correlationId, - playerId, - colonyId, - actualOwnerId: colony.player_id, - }); - - return res.status(403).json({ - success: false, - message: 'Access denied to this colony', - correlationId, - }); - } - - logger.info('Colony details retrieved', { - correlationId, - playerId, - colonyId, - colonyName: colony.name, - }); - - res.status(200).json({ - success: true, - message: 'Colony details retrieved successfully', - data: { - colony, - }, - correlationId, - }); -}); - -/** - * Construct a building in a colony - * POST /api/player/colonies/:colonyId/buildings - */ -const constructBuilding = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const colonyId = parseInt(req.params.colonyId); - const { building_type_id } = req.body; - - logger.info('Building construction request received', { - correlationId, - playerId, - colonyId, - building_type_id, - }); - - const colonyService = getColonyService(); - const building = await colonyService.constructBuilding( - colonyId, - building_type_id, - playerId, - correlationId, - ); - - logger.info('Building constructed successfully', { - correlationId, - playerId, - colonyId, - buildingId: building.id, - building_type_id, - }); - - res.status(201).json({ - success: true, - message: 'Building constructed successfully', - data: { - building, - }, - correlationId, - }); -}); - -/** - * Get available building types - * GET /api/player/buildings/types - */ -const getBuildingTypes = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - - logger.info('Building types request received', { - correlationId, - }); - - const colonyService = getColonyService(); - const buildingTypes = await colonyService.getAvailableBuildingTypes(correlationId); - - logger.info('Building types retrieved', { - correlationId, - count: buildingTypes.length, - }); - - res.status(200).json({ - success: true, - message: 'Building types retrieved successfully', - data: { - buildingTypes, - }, - correlationId, - }); -}); - -/** - * Get all planet types for colony creation - * GET /api/player/planets/types - */ -const getPlanetTypes = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - - logger.info('Planet types request received', { - correlationId, - }); - - try { - const planetTypes = await require('../../database/connection')('planet_types') - .select('*') - .where('is_active', true) - .orderBy('rarity_weight', 'desc'); - - logger.info('Planet types retrieved', { - correlationId, - count: planetTypes.length, - }); - - res.status(200).json({ - success: true, - message: 'Planet types retrieved successfully', - data: { - planetTypes, - }, - correlationId, - }); - - } catch (error) { - logger.error('Failed to retrieve planet types', { - correlationId, - error: error.message, - stack: error.stack, - }); - - res.status(500).json({ - success: false, - message: 'Failed to retrieve planet types', - correlationId, - }); - } -}); - -/** - * Get galaxy sectors for reference - * GET /api/player/galaxy/sectors - */ -const getGalaxySectors = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - - logger.info('Galaxy sectors request received', { - correlationId, - }); - - try { - const sectors = await require('../../database/connection')('galaxy_sectors') - .select('*') - .orderBy('danger_level', 'asc'); - - logger.info('Galaxy sectors retrieved', { - correlationId, - count: sectors.length, - }); - - res.status(200).json({ - success: true, - message: 'Galaxy sectors retrieved successfully', - data: { - sectors, - }, - correlationId, - }); - - } catch (error) { - logger.error('Failed to retrieve galaxy sectors', { - correlationId, - error: error.message, - stack: error.stack, - }); - - res.status(500).json({ - success: false, - message: 'Failed to retrieve galaxy sectors', - correlationId, - }); - } -}); - -module.exports = { - createColony, - getPlayerColonies, - getColonyDetails, - constructBuilding, - getBuildingTypes, - getPlanetTypes, - getGalaxySectors, -}; diff --git a/src/controllers/player/resource.controller.js b/src/controllers/player/resource.controller.js deleted file mode 100644 index 394a961..0000000 --- a/src/controllers/player/resource.controller.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Resource Controller - * Handles resource-related API endpoints for players - */ - -const ResourceService = require('../../services/resource/ResourceService'); -const { asyncHandler } = require('../../middleware/error.middleware'); -const logger = require('../../utils/logger'); -const serviceLocator = require('../../services/ServiceLocator'); - -// Create resource service with WebSocket integration -function getResourceService() { - const gameEventService = serviceLocator.get('gameEventService'); - return new ResourceService(gameEventService); -} - -/** - * Get player's current resources - * GET /api/player/resources - */ -const getPlayerResources = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - - logger.info('Player resources request received', { - correlationId, - playerId, - }); - - const resourceService = getResourceService(); - const resources = await resourceService.getPlayerResources(playerId, correlationId); - - logger.info('Player resources retrieved', { - correlationId, - playerId, - resourceCount: resources.length, - }); - - res.status(200).json({ - success: true, - message: 'Resources retrieved successfully', - data: { - resources, - }, - correlationId, - }); -}); - -/** - * Get player's resource summary (simplified view) - * GET /api/player/resources/summary - */ -const getPlayerResourceSummary = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - - logger.info('Player resource summary request received', { - correlationId, - playerId, - }); - - const resourceService = getResourceService(); - const summary = await resourceService.getPlayerResourceSummary(playerId, correlationId); - - logger.info('Player resource summary retrieved', { - correlationId, - playerId, - resourceTypes: Object.keys(summary), - }); - - res.status(200).json({ - success: true, - message: 'Resource summary retrieved successfully', - data: { - resources: summary, - }, - correlationId, - }); -}); - -/** - * Get player's resource production rates - * GET /api/player/resources/production - */ -const getResourceProduction = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - - logger.info('Resource production request received', { - correlationId, - playerId, - }); - - const resourceService = getResourceService(); - const production = await resourceService.calculatePlayerResourceProduction(playerId, correlationId); - - logger.info('Resource production calculated', { - correlationId, - playerId, - productionData: production, - }); - - res.status(200).json({ - success: true, - message: 'Resource production retrieved successfully', - data: { - production, - }, - correlationId, - }); -}); - -/** - * Add resources to player (for testing/admin purposes) - * POST /api/player/resources/add - */ -const addResources = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const { resources } = req.body; - - // Only allow in development environment - if (process.env.NODE_ENV !== 'development') { - logger.warn('Resource addition attempted in production', { - correlationId, - playerId, - }); - - return res.status(403).json({ - success: false, - message: 'Resource addition not allowed in production', - correlationId, - }); - } - - logger.info('Resource addition request received', { - correlationId, - playerId, - resources, - }); - - const resourceService = getResourceService(); - const updatedResources = await resourceService.addPlayerResources( - playerId, - resources, - correlationId, - ); - - logger.info('Resources added successfully', { - correlationId, - playerId, - updatedResources, - }); - - res.status(200).json({ - success: true, - message: 'Resources added successfully', - data: { - updatedResources, - }, - correlationId, - }); -}); - -/** - * Transfer resources between colonies - * POST /api/player/resources/transfer - */ -const transferResources = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - const playerId = req.user.playerId; - const { fromColonyId, toColonyId, resources } = req.body; - - logger.info('Resource transfer request received', { - correlationId, - playerId, - fromColonyId, - toColonyId, - resources, - }); - - const resourceService = getResourceService(); - const result = await resourceService.transferResourcesBetweenColonies( - fromColonyId, - toColonyId, - resources, - playerId, - correlationId, - ); - - logger.info('Resources transferred successfully', { - correlationId, - playerId, - fromColonyId, - toColonyId, - transferResult: result, - }); - - res.status(200).json({ - success: true, - message: 'Resources transferred successfully', - data: result, - correlationId, - }); -}); - -/** - * Get all available resource types - * GET /api/player/resources/types - */ -const getResourceTypes = asyncHandler(async (req, res) => { - const correlationId = req.correlationId; - - logger.info('Resource types request received', { - correlationId, - }); - - const resourceService = getResourceService(); - const resourceTypes = await resourceService.getResourceTypes(correlationId); - - logger.info('Resource types retrieved', { - correlationId, - count: resourceTypes.length, - }); - - res.status(200).json({ - success: true, - message: 'Resource types retrieved successfully', - data: { - resourceTypes, - }, - correlationId, - }); -}); - -module.exports = { - getPlayerResources, - getPlayerResourceSummary, - getResourceProduction, - addResources, - transferResources, - getResourceTypes, -}; diff --git a/src/controllers/websocket/connection.handler.js b/src/controllers/websocket/connection.handler.js index a8b8622..290bd13 100644 --- a/src/controllers/websocket/connection.handler.js +++ b/src/controllers/websocket/connection.handler.js @@ -12,34 +12,34 @@ const logger = require('../../utils/logger'); * @param {Object} io - Socket.IO server instance */ function handleConnection(socket, io) { - const correlationId = socket.correlationId; + const correlationId = socket.correlationId; + + logger.info('WebSocket connection established', { + correlationId, + socketId: socket.id, + ip: socket.handshake.address + }); - logger.info('WebSocket connection established', { - correlationId, - socketId: socket.id, - ip: socket.handshake.address, - }); + // Set up authentication handler + socket.on('authenticate', async (data) => { + await handleAuthentication(socket, data, correlationId); + }); - // Set up authentication handler - socket.on('authenticate', async (data) => { - await handleAuthentication(socket, data, correlationId); - }); + // Set up game event handlers + setupGameEventHandlers(socket, io, correlationId); - // Set up game event handlers - setupGameEventHandlers(socket, io, correlationId); + // Set up utility handlers + setupUtilityHandlers(socket, io, correlationId); - // Set up utility handlers - setupUtilityHandlers(socket, io, correlationId); + // Handle disconnection + socket.on('disconnect', (reason) => { + handleDisconnection(socket, reason, correlationId); + }); - // Handle disconnection - socket.on('disconnect', (reason) => { - handleDisconnection(socket, reason, correlationId); - }); - - // Handle connection errors - socket.on('error', (error) => { - handleConnectionError(socket, error, correlationId); - }); + // Handle connection errors + socket.on('error', (error) => { + handleConnectionError(socket, error, correlationId); + }); } /** @@ -49,67 +49,67 @@ function handleConnection(socket, io) { * @param {string} correlationId - Connection correlation ID */ async function handleAuthentication(socket, data, correlationId) { - try { - const { token } = data; + try { + const { token } = data; - if (!token) { - logger.warn('WebSocket authentication failed - no token provided', { - correlationId, - socketId: socket.id, - }); + if (!token) { + logger.warn('WebSocket authentication failed - no token provided', { + correlationId, + socketId: socket.id + }); - socket.emit('authentication_error', { - success: false, - message: 'Authentication token required', - }); - return; + socket.emit('authentication_error', { + success: false, + message: 'Authentication token required' + }); + return; + } + + // Verify the player token + const decoded = verifyPlayerToken(token); + + // Store player information in socket + socket.playerId = decoded.playerId; + socket.username = decoded.username; + socket.email = decoded.email; + socket.authenticated = true; + + // Join player-specific room + const playerRoom = `player:${decoded.playerId}`; + socket.join(playerRoom); + + logger.info('WebSocket authentication successful', { + correlationId, + socketId: socket.id, + playerId: decoded.playerId, + username: decoded.username + }); + + socket.emit('authenticated', { + success: true, + message: 'Authentication successful', + player: { + id: decoded.playerId, + username: decoded.username, + email: decoded.email + } + }); + + // Send initial game state or notifications + await sendInitialGameState(socket, decoded.playerId, correlationId); + + } catch (error) { + logger.warn('WebSocket authentication failed', { + correlationId, + socketId: socket.id, + error: error.message + }); + + socket.emit('authentication_error', { + success: false, + message: 'Authentication failed' + }); } - - // Verify the player token - const decoded = verifyPlayerToken(token); - - // Store player information in socket - socket.playerId = decoded.playerId; - socket.username = decoded.username; - socket.email = decoded.email; - socket.authenticated = true; - - // Join player-specific room - const playerRoom = `player:${decoded.playerId}`; - socket.join(playerRoom); - - logger.info('WebSocket authentication successful', { - correlationId, - socketId: socket.id, - playerId: decoded.playerId, - username: decoded.username, - }); - - socket.emit('authenticated', { - success: true, - message: 'Authentication successful', - player: { - id: decoded.playerId, - username: decoded.username, - email: decoded.email, - }, - }); - - // Send initial game state or notifications - await sendInitialGameState(socket, decoded.playerId, correlationId); - - } catch (error) { - logger.warn('WebSocket authentication failed', { - correlationId, - socketId: socket.id, - error: error.message, - }); - - socket.emit('authentication_error', { - success: false, - message: 'Authentication failed', - }); - } } /** @@ -119,131 +119,131 @@ async function handleAuthentication(socket, data, correlationId) { * @param {string} correlationId - Connection correlation ID */ function setupGameEventHandlers(socket, io, correlationId) { - // Colony updates - socket.on('subscribe_colony_updates', (data) => { - if (!socket.authenticated) { - socket.emit('error', { message: 'Authentication required' }); - return; - } + // Colony updates + socket.on('subscribe_colony_updates', (data) => { + if (!socket.authenticated) { + socket.emit('error', { message: 'Authentication required' }); + return; + } - const { colonyId } = data; - if (colonyId) { - const roomName = `colony:${colonyId}`; - socket.join(roomName); + const { colonyId } = data; + if (colonyId) { + const roomName = `colony:${colonyId}`; + socket.join(roomName); + + logger.debug('Player subscribed to colony updates', { + correlationId, + socketId: socket.id, + playerId: socket.playerId, + colonyId, + room: roomName + }); - logger.debug('Player subscribed to colony updates', { - correlationId, - socketId: socket.id, - playerId: socket.playerId, - colonyId, - room: roomName, - }); - - socket.emit('subscribed', { - type: 'colony_updates', - colonyId, - }); - } - }); - - // Fleet updates - socket.on('subscribe_fleet_updates', (data) => { - if (!socket.authenticated) { - socket.emit('error', { message: 'Authentication required' }); - return; - } - - const { fleetId } = data; - if (fleetId) { - const roomName = `fleet:${fleetId}`; - socket.join(roomName); - - logger.debug('Player subscribed to fleet updates', { - correlationId, - socketId: socket.id, - playerId: socket.playerId, - fleetId, - room: roomName, - }); - - socket.emit('subscribed', { - type: 'fleet_updates', - fleetId, - }); - } - }); - - // Galaxy sector updates - socket.on('subscribe_sector_updates', (data) => { - if (!socket.authenticated) { - socket.emit('error', { message: 'Authentication required' }); - return; - } - - const { sectorId } = data; - if (sectorId) { - const roomName = `sector:${sectorId}`; - socket.join(roomName); - - logger.debug('Player subscribed to sector updates', { - correlationId, - socketId: socket.id, - playerId: socket.playerId, - sectorId, - room: roomName, - }); - - socket.emit('subscribed', { - type: 'sector_updates', - sectorId, - }); - } - }); - - // Battle updates - socket.on('subscribe_battle_updates', (data) => { - if (!socket.authenticated) { - socket.emit('error', { message: 'Authentication required' }); - return; - } - - const { battleId } = data; - if (battleId) { - const roomName = `battle:${battleId}`; - socket.join(roomName); - - logger.debug('Player subscribed to battle updates', { - correlationId, - socketId: socket.id, - playerId: socket.playerId, - battleId, - room: roomName, - }); - - socket.emit('subscribed', { - type: 'battle_updates', - battleId, - }); - } - }); - - // Unsubscribe from updates - socket.on('unsubscribe', (data) => { - const { type, id } = data; - const roomName = `${type}:${id}`; - socket.leave(roomName); - - logger.debug('Player unsubscribed from updates', { - correlationId, - socketId: socket.id, - playerId: socket.playerId, - type, - id, - room: roomName, + socket.emit('subscribed', { + type: 'colony_updates', + colonyId: colonyId + }); + } }); - socket.emit('unsubscribed', { type, id }); - }); + // Fleet updates + socket.on('subscribe_fleet_updates', (data) => { + if (!socket.authenticated) { + socket.emit('error', { message: 'Authentication required' }); + return; + } + + const { fleetId } = data; + if (fleetId) { + const roomName = `fleet:${fleetId}`; + socket.join(roomName); + + logger.debug('Player subscribed to fleet updates', { + correlationId, + socketId: socket.id, + playerId: socket.playerId, + fleetId, + room: roomName + }); + + socket.emit('subscribed', { + type: 'fleet_updates', + fleetId: fleetId + }); + } + }); + + // Galaxy sector updates + socket.on('subscribe_sector_updates', (data) => { + if (!socket.authenticated) { + socket.emit('error', { message: 'Authentication required' }); + return; + } + + const { sectorId } = data; + if (sectorId) { + const roomName = `sector:${sectorId}`; + socket.join(roomName); + + logger.debug('Player subscribed to sector updates', { + correlationId, + socketId: socket.id, + playerId: socket.playerId, + sectorId, + room: roomName + }); + + socket.emit('subscribed', { + type: 'sector_updates', + sectorId: sectorId + }); + } + }); + + // Battle updates + socket.on('subscribe_battle_updates', (data) => { + if (!socket.authenticated) { + socket.emit('error', { message: 'Authentication required' }); + return; + } + + const { battleId } = data; + if (battleId) { + const roomName = `battle:${battleId}`; + socket.join(roomName); + + logger.debug('Player subscribed to battle updates', { + correlationId, + socketId: socket.id, + playerId: socket.playerId, + battleId, + room: roomName + }); + + socket.emit('subscribed', { + type: 'battle_updates', + battleId: battleId + }); + } + }); + + // Unsubscribe from updates + socket.on('unsubscribe', (data) => { + const { type, id } = data; + const roomName = `${type}:${id}`; + socket.leave(roomName); + + logger.debug('Player unsubscribed from updates', { + correlationId, + socketId: socket.id, + playerId: socket.playerId, + type, + id, + room: roomName + }); + + socket.emit('unsubscribed', { type, id }); + }); } /** @@ -253,58 +253,58 @@ function setupGameEventHandlers(socket, io, correlationId) { * @param {string} correlationId - Connection correlation ID */ function setupUtilityHandlers(socket, io, correlationId) { - // Ping/pong for connection testing - socket.on('ping', (data) => { - const timestamp = Date.now(); - socket.emit('pong', { - timestamp, - serverTime: new Date().toISOString(), - latency: data?.timestamp ? timestamp - data.timestamp : null, - }); - }); - - // Player status updates - socket.on('update_status', (data) => { - if (!socket.authenticated) { - socket.emit('error', { message: 'Authentication required' }); - return; - } - - const { status } = data; - if (['online', 'away', 'busy'].includes(status)) { - socket.playerStatus = status; - - logger.debug('Player status updated', { - correlationId, - socketId: socket.id, - playerId: socket.playerId, - status, - }); - - // Broadcast status to relevant rooms/players - // TODO: Implement player status broadcasting - } - }); - - // Chat/messaging - socket.on('send_message', async (data) => { - if (!socket.authenticated) { - socket.emit('error', { message: 'Authentication required' }); - return; - } - - // TODO: Implement real-time messaging - logger.debug('Message send requested', { - correlationId, - socketId: socket.id, - playerId: socket.playerId, - messageType: data.type, + // Ping/pong for connection testing + socket.on('ping', (data) => { + const timestamp = Date.now(); + socket.emit('pong', { + timestamp, + serverTime: new Date().toISOString(), + latency: data?.timestamp ? timestamp - data.timestamp : null + }); }); - socket.emit('message_error', { - message: 'Messaging feature not yet implemented', + // Player status updates + socket.on('update_status', (data) => { + if (!socket.authenticated) { + socket.emit('error', { message: 'Authentication required' }); + return; + } + + const { status } = data; + if (['online', 'away', 'busy'].includes(status)) { + socket.playerStatus = status; + + logger.debug('Player status updated', { + correlationId, + socketId: socket.id, + playerId: socket.playerId, + status + }); + + // Broadcast status to relevant rooms/players + // TODO: Implement player status broadcasting + } + }); + + // Chat/messaging + socket.on('send_message', async (data) => { + if (!socket.authenticated) { + socket.emit('error', { message: 'Authentication required' }); + return; + } + + // TODO: Implement real-time messaging + logger.debug('Message send requested', { + correlationId, + socketId: socket.id, + playerId: socket.playerId, + messageType: data.type + }); + + socket.emit('message_error', { + message: 'Messaging feature not yet implemented' + }); }); - }); } /** @@ -314,17 +314,17 @@ function setupUtilityHandlers(socket, io, correlationId) { * @param {string} correlationId - Connection correlation ID */ function handleDisconnection(socket, reason, correlationId) { - logger.info('WebSocket client disconnected', { - correlationId, - socketId: socket.id, - playerId: socket.playerId, - username: socket.username, - reason, - duration: socket.connectedAt ? Date.now() - socket.connectedAt : 0, - }); + logger.info('WebSocket client disconnected', { + correlationId, + socketId: socket.id, + playerId: socket.playerId, + username: socket.username, + reason, + duration: socket.connectedAt ? Date.now() - socket.connectedAt : 0 + }); - // TODO: Update player online status - // TODO: Clean up any player-specific subscriptions or states + // TODO: Update player online status + // TODO: Clean up any player-specific subscriptions or states } /** @@ -334,18 +334,18 @@ function handleDisconnection(socket, reason, correlationId) { * @param {string} correlationId - Connection correlation ID */ function handleConnectionError(socket, error, correlationId) { - logger.error('WebSocket connection error', { - correlationId, - socketId: socket.id, - playerId: socket.playerId, - error: error.message, - stack: error.stack, - }); + logger.error('WebSocket connection error', { + correlationId, + socketId: socket.id, + playerId: socket.playerId, + error: error.message, + stack: error.stack + }); - socket.emit('connection_error', { - message: 'Connection error occurred', - reconnect: true, - }); + socket.emit('connection_error', { + message: 'Connection error occurred', + reconnect: true + }); } /** @@ -355,53 +355,53 @@ function handleConnectionError(socket, error, correlationId) { * @param {string} correlationId - Connection correlation ID */ async function sendInitialGameState(socket, playerId, correlationId) { - try { - // TODO: Fetch and send initial game state - // This could include: - // - Player resources - // - Colony statuses - // - Fleet positions - // - Pending notifications - // - Current research - // - Active battles + try { + // TODO: Fetch and send initial game state + // This could include: + // - Player resources + // - Colony statuses + // - Fleet positions + // - Pending notifications + // - Current research + // - Active battles - const initialState = { - timestamp: new Date().toISOString(), - player: { - id: playerId, - online: true, - }, - gameState: { - // Placeholder for game state data - tick: Date.now(), - version: process.env.npm_package_version || '0.1.0', - }, - notifications: { - unread: 0, - recent: [], - }, - }; + const initialState = { + timestamp: new Date().toISOString(), + player: { + id: playerId, + online: true + }, + gameState: { + // Placeholder for game state data + tick: Date.now(), + version: process.env.npm_package_version || '0.1.0' + }, + notifications: { + unread: 0, + recent: [] + } + }; - socket.emit('initial_state', initialState); + socket.emit('initial_state', initialState); - logger.debug('Initial game state sent', { - correlationId, - socketId: socket.id, - playerId, - }); + logger.debug('Initial game state sent', { + correlationId, + socketId: socket.id, + playerId + }); - } catch (error) { - logger.error('Failed to send initial game state', { - correlationId, - socketId: socket.id, - playerId, - error: error.message, - }); + } catch (error) { + logger.error('Failed to send initial game state', { + correlationId, + socketId: socket.id, + playerId, + error: error.message + }); - socket.emit('error', { - message: 'Failed to load initial game state', - }); - } + socket.emit('error', { + message: 'Failed to load initial game state' + }); + } } /** @@ -412,35 +412,35 @@ async function sendInitialGameState(socket, playerId, correlationId) { * @param {Array} targetPlayers - Array of player IDs to notify */ function broadcastGameEvent(io, eventType, eventData, targetPlayers = []) { - const timestamp = new Date().toISOString(); + const timestamp = new Date().toISOString(); + + const broadcastData = { + type: eventType, + data: eventData, + timestamp + }; - const broadcastData = { - type: eventType, - data: eventData, - timestamp, - }; + if (targetPlayers.length > 0) { + // Send to specific players + targetPlayers.forEach(playerId => { + io.to(`player:${playerId}`).emit('game_event', broadcastData); + }); - if (targetPlayers.length > 0) { - // Send to specific players - targetPlayers.forEach(playerId => { - io.to(`player:${playerId}`).emit('game_event', broadcastData); - }); + logger.debug('Game event broadcast to specific players', { + eventType, + playerCount: targetPlayers.length + }); + } else { + // Broadcast to all authenticated players + io.emit('game_event', broadcastData); - logger.debug('Game event broadcast to specific players', { - eventType, - playerCount: targetPlayers.length, - }); - } else { - // Broadcast to all authenticated players - io.emit('game_event', broadcastData); - - logger.debug('Game event broadcast to all players', { - eventType, - }); - } + logger.debug('Game event broadcast to all players', { + eventType + }); + } } module.exports = { - handleConnection, - broadcastGameEvent, -}; + handleConnection, + broadcastGameEvent +}; \ No newline at end of file diff --git a/src/data/ship-designs.js b/src/data/ship-designs.js deleted file mode 100644 index f8c38d0..0000000 --- a/src/data/ship-designs.js +++ /dev/null @@ -1,551 +0,0 @@ -/** - * Ship Design Definitions - * Defines available ship designs, their stats, and research prerequisites - */ - -/** - * Ship classes and their base characteristics - */ -const SHIP_CLASSES = { - FIGHTER: 'fighter', - CORVETTE: 'corvette', - FRIGATE: 'frigate', - DESTROYER: 'destroyer', - CRUISER: 'cruiser', - BATTLESHIP: 'battleship', - CARRIER: 'carrier', - SUPPORT: 'support' -}; - -/** - * Hull types with base stats - */ -const HULL_TYPES = { - light: { - base_hp: 100, - base_armor: 10, - base_speed: 8, - size_modifier: 1.0, - cost_modifier: 1.0 - }, - medium: { - base_hp: 250, - base_armor: 25, - base_speed: 6, - size_modifier: 1.5, - cost_modifier: 1.3 - }, - heavy: { - base_hp: 500, - base_armor: 50, - base_speed: 4, - size_modifier: 2.0, - cost_modifier: 1.8 - }, - capital: { - base_hp: 1000, - base_armor: 100, - base_speed: 2, - size_modifier: 3.0, - cost_modifier: 2.5 - } -}; - -/** - * Ship design templates - * Each design includes: - * - id: Unique identifier - * - name: Display name - * - ship_class: Ship classification - * - hull_type: Hull type from HULL_TYPES - * - tech_requirements: Required technologies to build - * - components: Weapon and equipment loadout - * - base_cost: Resource cost to build - * - build_time: Construction time in minutes - * - stats: Calculated combat statistics - */ -const SHIP_DESIGNS = [ - // === BASIC DESIGNS (No tech requirements) === - { - id: 1, - name: 'Patrol Drone', - ship_class: SHIP_CLASSES.FIGHTER, - hull_type: 'light', - tech_requirements: [8], // Basic Defense - components: { - weapons: ['basic_laser'], - shields: ['basic_shield'], - engines: ['ion_drive'], - utilities: ['basic_sensors'] - }, - base_cost: { - scrap: 50, - energy: 25, - rare_elements: 2 - }, - build_time: 15, // 15 minutes - stats: { - hp: 120, - armor: 15, - shields: 25, - attack: 35, - defense: 20, - speed: 9, - evasion: 15 - }, - description: 'Light patrol craft for colony defense and scouting missions.' - }, - - { - id: 2, - name: 'Salvage Corvette', - ship_class: SHIP_CLASSES.CORVETTE, - hull_type: 'light', - tech_requirements: [2], // Advanced Salvaging - components: { - weapons: ['mining_laser'], - shields: ['basic_shield'], - engines: ['ion_drive'], - utilities: ['salvage_bay', 'basic_sensors'] - }, - base_cost: { - scrap: 80, - energy: 40, - rare_elements: 3 - }, - build_time: 25, - stats: { - hp: 150, - armor: 20, - shields: 30, - attack: 20, - defense: 25, - speed: 7, - cargo_capacity: 100 - }, - description: 'Specialized ship for resource collection and salvage operations.' - }, - - { - id: 3, - name: 'Construction Corvette', - ship_class: SHIP_CLASSES.SUPPORT, - hull_type: 'medium', - tech_requirements: [10], // Military Engineering - components: { - weapons: ['basic_laser'], - shields: ['reinforced_shield'], - engines: ['fusion_drive'], - utilities: ['construction_bay', 'engineering_suite'] - }, - base_cost: { - scrap: 150, - energy: 100, - rare_elements: 8 - }, - build_time: 45, - stats: { - hp: 300, - armor: 40, - shields: 50, - attack: 25, - defense: 35, - speed: 5, - construction_bonus: 0.2 - }, - description: 'Engineering vessel capable of rapid field construction and repairs.' - }, - - // === TIER 2 DESIGNS === - { - id: 4, - name: 'Laser Frigate', - ship_class: SHIP_CLASSES.FRIGATE, - hull_type: 'medium', - tech_requirements: [12], // Energy Weapons - components: { - weapons: ['pulse_laser', 'point_defense_laser'], - shields: ['energy_shield'], - engines: ['fusion_drive'], - utilities: ['targeting_computer', 'advanced_sensors'] - }, - base_cost: { - scrap: 200, - energy: 150, - rare_elements: 15 - }, - build_time: 60, - stats: { - hp: 350, - armor: 35, - shields: 80, - attack: 65, - defense: 40, - speed: 6, - energy_weapon_bonus: 0.15 - }, - description: 'Fast attack vessel armed with advanced energy weapons.' - }, - - { - id: 5, - name: 'Energy Destroyer', - ship_class: SHIP_CLASSES.DESTROYER, - hull_type: 'heavy', - tech_requirements: [12], // Energy Weapons - components: { - weapons: ['heavy_laser', 'dual_pulse_laser'], - shields: ['reinforced_energy_shield'], - engines: ['plasma_drive'], - utilities: ['fire_control_system', 'ECM_suite'] - }, - base_cost: { - scrap: 350, - energy: 250, - rare_elements: 25 - }, - build_time: 90, - stats: { - hp: 600, - armor: 60, - shields: 120, - attack: 95, - defense: 55, - speed: 5, - shield_penetration: 0.2 - }, - description: 'Heavy warship designed for ship-to-ship combat.' - }, - - { - id: 6, - name: 'Command Cruiser', - ship_class: SHIP_CLASSES.CRUISER, - hull_type: 'heavy', - tech_requirements: [13], // Fleet Command - components: { - weapons: ['twin_laser_turret', 'missile_launcher'], - shields: ['command_shield'], - engines: ['advanced_fusion_drive'], - utilities: ['command_center', 'fleet_coordination', 'long_range_sensors'] - }, - base_cost: { - scrap: 500, - energy: 350, - rare_elements: 40 - }, - build_time: 120, - stats: { - hp: 800, - armor: 80, - shields: 150, - attack: 75, - defense: 70, - speed: 4, - fleet_command_bonus: 0.25 - }, - description: 'Fleet command vessel that provides tactical coordination bonuses.' - }, - - // === TIER 3 DESIGNS === - { - id: 7, - name: 'Industrial Vessel', - ship_class: SHIP_CLASSES.SUPPORT, - hull_type: 'heavy', - tech_requirements: [11], // Advanced Manufacturing - components: { - weapons: ['defensive_turret'], - shields: ['industrial_shield'], - engines: ['heavy_fusion_drive'], - utilities: ['manufacturing_bay', 'resource_processor', 'repair_facility'] - }, - base_cost: { - scrap: 400, - energy: 300, - rare_elements: 35 - }, - build_time: 100, - stats: { - hp: 700, - armor: 70, - shields: 100, - attack: 40, - defense: 60, - speed: 3, - manufacturing_bonus: 0.3, - repair_capability: true - }, - description: 'Mobile factory ship capable of resource processing and fleet repairs.' - }, - - { - id: 8, - name: 'Tactical Carrier', - ship_class: SHIP_CLASSES.CARRIER, - hull_type: 'capital', - tech_requirements: [18], // Advanced Tactics - components: { - weapons: ['carrier_defense_array'], - shields: ['capital_shield'], - engines: ['capital_drive'], - utilities: ['flight_deck', 'tactical_computer', 'hangar_bay'] - }, - base_cost: { - scrap: 800, - energy: 600, - rare_elements: 60 - }, - build_time: 180, - stats: { - hp: 1200, - armor: 120, - shields: 200, - attack: 60, - defense: 90, - speed: 3, - fighter_capacity: 20, - first_strike_bonus: 0.3 - }, - description: 'Capital ship that launches fighter squadrons and provides tactical support.' - }, - - // === TIER 4 DESIGNS === - { - id: 9, - name: 'Plasma Battleship', - ship_class: SHIP_CLASSES.BATTLESHIP, - hull_type: 'capital', - tech_requirements: [17], // Plasma Technology - components: { - weapons: ['plasma_cannon', 'plasma_torpedo_launcher'], - shields: ['plasma_shield'], - engines: ['plasma_drive'], - utilities: ['targeting_matrix', 'armor_plating'] - }, - base_cost: { - scrap: 1000, - energy: 800, - rare_elements: 80 - }, - build_time: 240, - stats: { - hp: 1500, - armor: 150, - shields: 250, - attack: 140, - defense: 100, - speed: 2, - plasma_weapon_damage: 1.2, - armor_penetration: 0.8 - }, - description: 'Devastating capital ship armed with advanced plasma weaponry.' - }, - - { - id: 10, - name: 'Defense Satellite', - ship_class: SHIP_CLASSES.SUPPORT, - hull_type: 'medium', - tech_requirements: [20], // Orbital Defense - components: { - weapons: ['orbital_laser', 'missile_battery'], - shields: ['satellite_shield'], - engines: ['station_keeping'], - utilities: ['orbital_platform', 'early_warning'] - }, - base_cost: { - scrap: 600, - energy: 400, - rare_elements: 50 - }, - build_time: 150, - stats: { - hp: 400, - armor: 80, - shields: 120, - attack: 100, - defense: 120, - speed: 0, // Stationary - orbital_defense_bonus: 2.0, - immobile: true - }, - description: 'Orbital defense platform providing powerful planetary protection.' - }, - - // === TIER 5 DESIGNS === - { - id: 11, - name: 'Dreadnought', - ship_class: SHIP_CLASSES.BATTLESHIP, - hull_type: 'capital', - tech_requirements: [21], // Strategic Warfare - components: { - weapons: ['super_plasma_cannon', 'strategic_missile_array'], - shields: ['dreadnought_shield'], - engines: ['quantum_drive'], - utilities: ['strategic_computer', 'command_suite', 'fleet_coordination'] - }, - base_cost: { - scrap: 2000, - energy: 1500, - rare_elements: 150 - }, - build_time: 360, - stats: { - hp: 2500, - armor: 200, - shields: 400, - attack: 200, - defense: 150, - speed: 3, - supreme_commander_bonus: 1.0, - fleet_command_bonus: 0.5 - }, - description: 'Ultimate warship representing the pinnacle of military engineering.' - }, - - { - id: 12, - name: 'Nanite Swarm', - ship_class: SHIP_CLASSES.SUPPORT, - hull_type: 'light', - tech_requirements: [16], // Nanotechnology - components: { - weapons: ['nanite_disassembler'], - shields: ['adaptive_nanoshield'], - engines: ['nanite_propulsion'], - utilities: ['self_replication', 'matter_reconstruction'] - }, - base_cost: { - scrap: 300, - energy: 400, - rare_elements: 100 - }, - build_time: 90, - stats: { - hp: 200, - armor: 30, - shields: 80, - attack: 80, - defense: 40, - speed: 10, - self_repair: 0.3, - construction_efficiency: 0.8 - }, - description: 'Self-replicating nanomachine swarm capable of rapid construction and repair.' - } -]; - -/** - * Helper functions for ship design management - */ - -/** - * Get ship design by ID - * @param {number} designId - Ship design ID - * @returns {Object|null} Ship design data or null if not found - */ -function getShipDesignById(designId) { - return SHIP_DESIGNS.find(design => design.id === designId) || null; -} - -/** - * Get ship designs by class - * @param {string} shipClass - Ship class - * @returns {Array} Array of ship designs in the class - */ -function getShipDesignsByClass(shipClass) { - return SHIP_DESIGNS.filter(design => design.ship_class === shipClass); -} - -/** - * Get available ship designs for a player based on completed research - * @param {Array} completedTechIds - Array of completed technology IDs - * @returns {Array} Array of available ship designs - */ -function getAvailableShipDesigns(completedTechIds) { - return SHIP_DESIGNS.filter(design => { - // Check if all required technologies are researched - return design.tech_requirements.every(techId => - completedTechIds.includes(techId) - ); - }); -} - -/** - * Validate if a ship design can be built - * @param {number} designId - Ship design ID - * @param {Array} completedTechIds - Array of completed technology IDs - * @returns {Object} Validation result with success/error - */ -function validateShipDesignAvailability(designId, completedTechIds) { - const design = getShipDesignById(designId); - - if (!design) { - return { - valid: false, - error: 'Ship design not found' - }; - } - - const missingTechs = design.tech_requirements.filter(techId => - !completedTechIds.includes(techId) - ); - - if (missingTechs.length > 0) { - return { - valid: false, - error: 'Missing required technologies', - missingTechnologies: missingTechs - }; - } - - return { - valid: true, - design: design - }; -} - -/** - * Calculate ship construction cost with bonuses - * @param {Object} design - Ship design - * @param {Object} bonuses - Construction bonuses from technologies - * @returns {Object} Modified construction costs - */ -function calculateShipCost(design, bonuses = {}) { - const baseCost = design.base_cost; - const costReduction = bonuses.construction_cost_reduction || 0; - - const modifiedCost = {}; - Object.entries(baseCost).forEach(([resource, cost]) => { - modifiedCost[resource] = Math.max(1, Math.floor(cost * (1 - costReduction))); - }); - - return modifiedCost; -} - -/** - * Calculate ship build time with bonuses - * @param {Object} design - Ship design - * @param {Object} bonuses - Construction bonuses from technologies - * @returns {number} Modified build time in minutes - */ -function calculateBuildTime(design, bonuses = {}) { - const baseTime = design.build_time; - const speedBonus = bonuses.construction_speed_bonus || 0; - - return Math.max(5, Math.floor(baseTime * (1 - speedBonus))); -} - -module.exports = { - SHIP_DESIGNS, - SHIP_CLASSES, - HULL_TYPES, - getShipDesignById, - getShipDesignsByClass, - getAvailableShipDesigns, - validateShipDesignAvailability, - calculateShipCost, - calculateBuildTime -}; \ No newline at end of file diff --git a/src/data/technologies.js b/src/data/technologies.js deleted file mode 100644 index 81eea39..0000000 --- a/src/data/technologies.js +++ /dev/null @@ -1,756 +0,0 @@ -/** - * Technology Definitions - * Defines the complete technology tree for the game - */ - -/** - * Technology categories - */ -const TECH_CATEGORIES = { - MILITARY: 'military', - INDUSTRIAL: 'industrial', - SOCIAL: 'social', - EXPLORATION: 'exploration' -}; - -/** - * Technology data structure: - * - id: Unique identifier (matches database) - * - name: Display name - * - description: Technology description - * - category: Technology category - * - tier: Technology tier (1-5) - * - prerequisites: Array of technology IDs required - * - research_cost: Resource costs to research - * - research_time: Time in minutes to complete - * - effects: Benefits granted by this technology - * - unlocks: Buildings, ships, or other content unlocked - */ -const TECHNOLOGIES = [ - // === TIER 1 TECHNOLOGIES === - { - id: 1, - name: 'Resource Efficiency', - description: 'Improve resource extraction and processing efficiency across all colonies.', - category: TECH_CATEGORIES.INDUSTRIAL, - tier: 1, - prerequisites: [], - research_cost: { - scrap: 100, - energy: 50, - data_cores: 5 - }, - research_time: 30, // 30 minutes - effects: { - resource_production_bonus: 0.1, // +10% to all resource production - storage_efficiency: 0.05 // +5% storage capacity - }, - unlocks: { - buildings: [], - ships: [], - technologies: [2, 3] // Unlocks Advanced Salvaging and Energy Grid - } - }, - - { - id: 2, - name: 'Advanced Salvaging', - description: 'Develop better techniques for extracting materials from ruins and debris.', - category: TECH_CATEGORIES.INDUSTRIAL, - tier: 1, - prerequisites: [1], // Requires Resource Efficiency - research_cost: { - scrap: 150, - energy: 75, - data_cores: 10 - }, - research_time: 45, - effects: { - scrap_production_bonus: 0.25, // +25% scrap production - salvage_yard_efficiency: 0.2 // +20% salvage yard efficiency - }, - unlocks: { - buildings: ['advanced_salvage_yard'], - ships: [], - technologies: [6] // Unlocks Industrial Automation - } - }, - - { - id: 3, - name: 'Energy Grid', - description: 'Establish efficient energy distribution networks across colony infrastructure.', - category: TECH_CATEGORIES.INDUSTRIAL, - tier: 1, - prerequisites: [1], // Requires Resource Efficiency - research_cost: { - scrap: 120, - energy: 100, - data_cores: 8 - }, - research_time: 40, - effects: { - energy_production_bonus: 0.2, // +20% energy production - power_plant_efficiency: 0.15 // +15% power plant efficiency - }, - unlocks: { - buildings: ['power_grid'], - ships: [], - technologies: [7] // Unlocks Advanced Power Systems - } - }, - - { - id: 4, - name: 'Colony Management', - description: 'Develop efficient administrative systems for colony operations.', - category: TECH_CATEGORIES.SOCIAL, - tier: 1, - prerequisites: [], - research_cost: { - scrap: 80, - energy: 60, - data_cores: 12 - }, - research_time: 35, - effects: { - population_growth_bonus: 0.15, // +15% population growth - morale_bonus: 5, // +5 base morale - command_efficiency: 0.1 // +10% to all colony operations - }, - unlocks: { - buildings: ['administrative_center'], - ships: [], - technologies: [5, 8] // Unlocks Population Growth and Basic Defense - } - }, - - { - id: 5, - name: 'Population Growth', - description: 'Improve living conditions and healthcare to support larger populations.', - category: TECH_CATEGORIES.SOCIAL, - tier: 1, - prerequisites: [4], // Requires Colony Management - research_cost: { - scrap: 100, - energy: 80, - data_cores: 15 - }, - research_time: 50, - effects: { - max_population_bonus: 0.2, // +20% max population per colony - housing_efficiency: 0.25, // +25% housing capacity - growth_rate_bonus: 0.3 // +30% population growth rate - }, - unlocks: { - buildings: ['residential_complex'], - ships: [], - technologies: [9] // Unlocks Social Engineering - } - }, - - // === TIER 2 TECHNOLOGIES === - { - id: 6, - name: 'Industrial Automation', - description: 'Implement automated systems for resource processing and manufacturing.', - category: TECH_CATEGORIES.INDUSTRIAL, - tier: 2, - prerequisites: [2], // Requires Advanced Salvaging - research_cost: { - scrap: 250, - energy: 200, - data_cores: 25, - rare_elements: 5 - }, - research_time: 90, - effects: { - production_automation_bonus: 0.3, // +30% production efficiency - maintenance_cost_reduction: 0.15, // -15% building maintenance - worker_efficiency: 0.2 // +20% worker productivity - }, - unlocks: { - buildings: ['automated_factory'], - ships: [], - technologies: [11] // Unlocks Advanced Manufacturing - } - }, - - { - id: 7, - name: 'Advanced Power Systems', - description: 'Develop high-efficiency power generation and distribution technology.', - category: TECH_CATEGORIES.INDUSTRIAL, - tier: 2, - prerequisites: [3], // Requires Energy Grid - research_cost: { - scrap: 200, - energy: 300, - data_cores: 20, - rare_elements: 8 - }, - research_time: 85, - effects: { - energy_efficiency: 0.4, // +40% energy production - power_consumption_reduction: 0.2, // -20% building power consumption - grid_stability: 0.25 // +25% power grid efficiency - }, - unlocks: { - buildings: ['power_core'], - ships: [], - technologies: [12] // Unlocks Energy Weapons - } - }, - - { - id: 8, - name: 'Basic Defense', - description: 'Establish fundamental defensive systems and protocols.', - category: TECH_CATEGORIES.MILITARY, - tier: 1, - prerequisites: [4], // Requires Colony Management - research_cost: { - scrap: 150, - energy: 120, - data_cores: 10, - rare_elements: 3 - }, - research_time: 60, - effects: { - defense_rating_bonus: 25, // +25 base defense rating - garrison_efficiency: 0.2, // +20% defensive unit effectiveness - early_warning: 0.15 // +15% detection range - }, - unlocks: { - buildings: ['guard_post'], - ships: ['patrol_drone'], - technologies: [10, 13] // Unlocks Military Engineering and Fleet Command - } - }, - - { - id: 9, - name: 'Social Engineering', - description: 'Advanced techniques for managing large populations and maintaining order.', - category: TECH_CATEGORIES.SOCIAL, - tier: 2, - prerequisites: [5], // Requires Population Growth - research_cost: { - scrap: 180, - energy: 150, - data_cores: 30, - rare_elements: 5 - }, - research_time: 75, - effects: { - morale_stability: 0.3, // +30% morale stability - civil_unrest_reduction: 0.4, // -40% civil unrest chance - loyalty_bonus: 10 // +10 base loyalty - }, - unlocks: { - buildings: ['propaganda_center'], - ships: [], - technologies: [14] // Unlocks Advanced Governance - } - }, - - { - id: 10, - name: 'Military Engineering', - description: 'Develop specialized engineering corps for military construction and logistics.', - category: TECH_CATEGORIES.MILITARY, - tier: 2, - prerequisites: [8], // Requires Basic Defense - research_cost: { - scrap: 300, - energy: 200, - data_cores: 25, - rare_elements: 10 - }, - research_time: 100, - effects: { - fortification_bonus: 0.5, // +50% defensive structure effectiveness - construction_speed_military: 0.3, // +30% military building construction speed - repair_efficiency: 0.25 // +25% repair speed - }, - unlocks: { - buildings: ['fortress_wall', 'bunker_complex'], - ships: ['construction_corvette'], - technologies: [15] // Unlocks Heavy Fortifications - } - }, - - // === TIER 3 TECHNOLOGIES === - { - id: 11, - name: 'Advanced Manufacturing', - description: 'Cutting-edge manufacturing processes for complex components and systems.', - category: TECH_CATEGORIES.INDUSTRIAL, - tier: 3, - prerequisites: [6], // Requires Industrial Automation - research_cost: { - scrap: 500, - energy: 400, - data_cores: 50, - rare_elements: 20 - }, - research_time: 150, - effects: { - production_quality_bonus: 0.4, // +40% production output quality - rare_element_efficiency: 0.3, // +30% rare element processing - manufacturing_speed: 0.25 // +25% manufacturing speed - }, - unlocks: { - buildings: ['nanotechnology_lab'], - ships: ['industrial_vessel'], - technologies: [16] // Unlocks Nanotechnology - } - }, - - { - id: 12, - name: 'Energy Weapons', - description: 'Harness advanced energy systems for military applications.', - category: TECH_CATEGORIES.MILITARY, - tier: 3, - prerequisites: [7, 8], // Requires Advanced Power Systems and Basic Defense - research_cost: { - scrap: 400, - energy: 600, - data_cores: 40, - rare_elements: 25 - }, - research_time: 140, - effects: { - weapon_power_bonus: 0.6, // +60% energy weapon damage - energy_weapon_efficiency: 0.3, // +30% energy weapon efficiency - shield_penetration: 0.2 // +20% shield penetration - }, - unlocks: { - buildings: ['weapon_testing_facility'], - ships: ['laser_frigate', 'energy_destroyer'], - technologies: [17] // Unlocks Plasma Technology - } - }, - - { - id: 13, - name: 'Fleet Command', - description: 'Develop command and control systems for coordinating multiple vessels.', - category: TECH_CATEGORIES.MILITARY, - tier: 2, - prerequisites: [8], // Requires Basic Defense - research_cost: { - scrap: 350, - energy: 250, - data_cores: 35, - rare_elements: 15 - }, - research_time: 110, - effects: { - fleet_coordination_bonus: 0.25, // +25% fleet combat effectiveness - command_capacity: 2, // +2 ships per fleet - tactical_bonus: 0.15 // +15% tactical combat bonus - }, - unlocks: { - buildings: ['fleet_command_center'], - ships: ['command_cruiser'], - technologies: [18] // Unlocks Advanced Tactics - } - }, - - { - id: 14, - name: 'Advanced Governance', - description: 'Sophisticated systems for managing large interstellar territories.', - category: TECH_CATEGORIES.SOCIAL, - tier: 3, - prerequisites: [9], // Requires Social Engineering - research_cost: { - scrap: 300, - energy: 250, - data_cores: 60, - rare_elements: 10 - }, - research_time: 130, - effects: { - colony_limit_bonus: 2, // +2 additional colonies - administrative_efficiency: 0.35, // +35% administrative efficiency - tax_collection_bonus: 0.2 // +20% resource collection efficiency - }, - unlocks: { - buildings: ['capitol_building'], - ships: [], - technologies: [19] // Unlocks Interstellar Communications - } - }, - - { - id: 15, - name: 'Heavy Fortifications', - description: 'Massive defensive structures capable of withstanding concentrated attacks.', - category: TECH_CATEGORIES.MILITARY, - tier: 3, - prerequisites: [10], // Requires Military Engineering - research_cost: { - scrap: 600, - energy: 400, - data_cores: 30, - rare_elements: 35 - }, - research_time: 160, - effects: { - defensive_structure_bonus: 1.0, // +100% defensive structure effectiveness - siege_resistance: 0.5, // +50% resistance to siege weapons - structural_integrity: 0.4 // +40% building durability - }, - unlocks: { - buildings: ['planetary_shield', 'fortress_citadel'], - ships: [], - technologies: [20] // Unlocks Orbital Defense - } - }, - - // === TIER 4 TECHNOLOGIES === - { - id: 16, - name: 'Nanotechnology', - description: 'Molecular-scale engineering for unprecedented precision manufacturing.', - category: TECH_CATEGORIES.INDUSTRIAL, - tier: 4, - prerequisites: [11], // Requires Advanced Manufacturing - research_cost: { - scrap: 800, - energy: 600, - data_cores: 100, - rare_elements: 50 - }, - research_time: 200, - effects: { - construction_efficiency: 0.8, // +80% construction efficiency - material_optimization: 0.6, // +60% material efficiency - self_repair: 0.3 // +30% self-repair capability - }, - unlocks: { - buildings: ['nanofabrication_plant'], - ships: ['nanite_swarm'], - technologies: [] // Top tier technology - } - }, - - { - id: 17, - name: 'Plasma Technology', - description: 'Harness the power of plasma for weapons and energy systems.', - category: TECH_CATEGORIES.MILITARY, - tier: 4, - prerequisites: [12], // Requires Energy Weapons - research_cost: { - scrap: 700, - energy: 1000, - data_cores: 80, - rare_elements: 60 - }, - research_time: 180, - effects: { - plasma_weapon_damage: 1.2, // +120% plasma weapon damage - energy_efficiency: 0.4, // +40% weapon energy efficiency - armor_penetration: 0.8 // +80% armor penetration - }, - unlocks: { - buildings: ['plasma_research_lab'], - ships: ['plasma_battleship'], - technologies: [] // Top tier technology - } - }, - - { - id: 18, - name: 'Advanced Tactics', - description: 'Revolutionary military doctrines and battlefield coordination systems.', - category: TECH_CATEGORIES.MILITARY, - tier: 3, - prerequisites: [13], // Requires Fleet Command - research_cost: { - scrap: 500, - energy: 350, - data_cores: 70, - rare_elements: 25 - }, - research_time: 170, - effects: { - combat_effectiveness: 0.5, // +50% overall combat effectiveness - first_strike_bonus: 0.3, // +30% first strike damage - retreat_efficiency: 0.4 // +40% successful retreat chance - }, - unlocks: { - buildings: ['war_college'], - ships: ['tactical_carrier'], - technologies: [21] // Unlocks Strategic Warfare - } - }, - - { - id: 19, - name: 'Interstellar Communications', - description: 'Instantaneous communication across galactic distances.', - category: TECH_CATEGORIES.EXPLORATION, - tier: 3, - prerequisites: [14], // Requires Advanced Governance - research_cost: { - scrap: 400, - energy: 500, - data_cores: 80, - rare_elements: 30 - }, - research_time: 145, - effects: { - communication_range: 'unlimited', // Unlimited communication range - coordination_bonus: 0.3, // +30% multi-colony coordination - intelligence_gathering: 0.4 // +40% intelligence effectiveness - }, - unlocks: { - buildings: ['quantum_communicator'], - ships: ['intelligence_vessel'], - technologies: [22] // Unlocks Quantum Computing - } - }, - - { - id: 20, - name: 'Orbital Defense', - description: 'Space-based defensive platforms and orbital weapon systems.', - category: TECH_CATEGORIES.MILITARY, - tier: 4, - prerequisites: [15], // Requires Heavy Fortifications - research_cost: { - scrap: 900, - energy: 700, - data_cores: 60, - rare_elements: 80 - }, - research_time: 220, - effects: { - orbital_defense_bonus: 2.0, // +200% orbital defense effectiveness - space_superiority: 0.6, // +60% space combat bonus - planetary_bombardment_resistance: 0.8 // +80% resistance to bombardment - }, - unlocks: { - buildings: ['orbital_defense_platform'], - ships: ['defense_satellite'], - technologies: [] // Top tier technology - } - }, - - // === TIER 5 TECHNOLOGIES === - { - id: 21, - name: 'Strategic Warfare', - description: 'Ultimate military doctrine combining all aspects of interstellar warfare.', - category: TECH_CATEGORIES.MILITARY, - tier: 5, - prerequisites: [18, 17], // Requires Advanced Tactics and Plasma Technology - research_cost: { - scrap: 1500, - energy: 1200, - data_cores: 150, - rare_elements: 100 - }, - research_time: 300, - effects: { - supreme_commander_bonus: 1.0, // +100% all military bonuses - multi_front_warfare: 0.5, // +50% effectiveness in multiple battles - victory_conditions: 'unlocked' // Unlocks victory condition paths - }, - unlocks: { - buildings: ['supreme_command'], - ships: ['dreadnought'], - technologies: [] // Ultimate technology - } - }, - - { - id: 22, - name: 'Quantum Computing', - description: 'Harness quantum mechanics for unprecedented computational power.', - category: TECH_CATEGORIES.EXPLORATION, - tier: 4, - prerequisites: [19], // Requires Interstellar Communications - research_cost: { - scrap: 1000, - energy: 800, - data_cores: 200, - rare_elements: 75 - }, - research_time: 250, - effects: { - research_speed_bonus: 0.8, // +80% research speed - data_processing_bonus: 1.5, // +150% data core efficiency - prediction_algorithms: 0.6 // +60% strategic planning bonus - }, - unlocks: { - buildings: ['quantum_computer'], - ships: ['research_vessel'], - technologies: [23] // Unlocks Technological Singularity - } - }, - - { - id: 23, - name: 'Technological Singularity', - description: 'Achieve the ultimate fusion of organic and artificial intelligence.', - category: TECH_CATEGORIES.EXPLORATION, - tier: 5, - prerequisites: [22, 16], // Requires Quantum Computing and Nanotechnology - research_cost: { - scrap: 2000, - energy: 1500, - data_cores: 300, - rare_elements: 150 - }, - research_time: 400, - effects: { - transcendence_bonus: 2.0, // +200% to all bonuses - reality_manipulation: 'unlocked', // Unlocks reality manipulation abilities - godlike_powers: 'activated' // Ultimate game-ending technology - }, - unlocks: { - buildings: ['singularity_core'], - ships: ['transcendent_entity'], - technologies: [] // Ultimate endgame technology - } - } -]; - -/** - * Helper functions for technology management - */ - -/** - * Get technology by ID - * @param {number} techId - Technology ID - * @returns {Object|null} Technology data or null if not found - */ -function getTechnologyById(techId) { - return TECHNOLOGIES.find(tech => tech.id === techId) || null; -} - -/** - * Get technologies by category - * @param {string} category - Technology category - * @returns {Array} Array of technologies in the category - */ -function getTechnologiesByCategory(category) { - return TECHNOLOGIES.filter(tech => tech.category === category); -} - -/** - * Get technologies by tier - * @param {number} tier - Technology tier (1-5) - * @returns {Array} Array of technologies in the tier - */ -function getTechnologiesByTier(tier) { - return TECHNOLOGIES.filter(tech => tech.tier === tier); -} - -/** - * Get available technologies for a player based on completed research - * @param {Array} completedTechIds - Array of completed technology IDs - * @returns {Array} Array of available technologies - */ -function getAvailableTechnologies(completedTechIds) { - return TECHNOLOGIES.filter(tech => { - // Check if already completed - if (completedTechIds.includes(tech.id)) { - return false; - } - - // Check if all prerequisites are met - return tech.prerequisites.every(prereqId => - completedTechIds.includes(prereqId) - ); - }); -} - -/** - * Validate if a technology can be researched - * @param {number} techId - Technology ID - * @param {Array} completedTechIds - Array of completed technology IDs - * @returns {Object} Validation result with success/error - */ -function validateTechnologyResearch(techId, completedTechIds) { - const tech = getTechnologyById(techId); - - if (!tech) { - return { - valid: false, - error: 'Technology not found' - }; - } - - if (completedTechIds.includes(techId)) { - return { - valid: false, - error: 'Technology already researched' - }; - } - - const missingPrereqs = tech.prerequisites.filter(prereqId => - !completedTechIds.includes(prereqId) - ); - - if (missingPrereqs.length > 0) { - return { - valid: false, - error: 'Missing prerequisites', - missingPrerequisites: missingPrereqs - }; - } - - return { - valid: true, - technology: tech - }; -} - -/** - * Calculate total research bonuses from completed technologies - * @param {Array} completedTechIds - Array of completed technology IDs - * @returns {Object} Combined effects from all completed technologies - */ -function calculateResearchBonuses(completedTechIds) { - const bonuses = { - resource_production_bonus: 0, - scrap_production_bonus: 0, - energy_production_bonus: 0, - defense_rating_bonus: 0, - population_growth_bonus: 0, - research_speed_bonus: 0, - // Add more bonus types as needed - }; - - completedTechIds.forEach(techId => { - const tech = getTechnologyById(techId); - if (tech && tech.effects) { - Object.entries(tech.effects).forEach(([effectKey, effectValue]) => { - if (typeof effectValue === 'number' && bonuses.hasOwnProperty(effectKey)) { - bonuses[effectKey] += effectValue; - } - }); - } - }); - - return bonuses; -} - -module.exports = { - TECHNOLOGIES, - TECH_CATEGORIES, - getTechnologyById, - getTechnologiesByCategory, - getTechnologiesByTier, - getAvailableTechnologies, - validateTechnologyResearch, - calculateResearchBonuses -}; \ No newline at end of file diff --git a/src/database/connection.js b/src/database/connection.js index 3c6c23d..0636f86 100644 --- a/src/database/connection.js +++ b/src/database/connection.js @@ -6,7 +6,7 @@ const environment = process.env.NODE_ENV || 'development'; const config = knexConfig[environment]; if (!config) { - throw new Error(`No database configuration found for environment: ${environment}`); + throw new Error(`No database configuration found for environment: ${environment}`); } const db = knex(config); @@ -19,37 +19,37 @@ let isConnected = false; * @returns {Promise} Connection success status */ async function initializeDatabase() { - try { - if (isConnected) { - logger.info('Database already connected'); - return true; + try { + if (isConnected) { + logger.info('Database already connected'); + return true; + } + + // Test database connection + await db.raw('SELECT 1'); + isConnected = true; + + logger.info('Database connection established successfully', { + environment, + host: config.connection.host, + database: config.connection.database, + pool: { + min: config.pool?.min || 0, + max: config.pool?.max || 10 + } + }); + + return true; + } catch (error) { + logger.error('Failed to establish database connection', { + environment, + host: config.connection?.host, + database: config.connection?.database, + error: error.message, + stack: error.stack + }); + throw error; } - - // Test database connection - await db.raw('SELECT 1'); - isConnected = true; - - logger.info('Database connection established successfully', { - environment, - host: config.connection.host, - database: config.connection.database, - pool: { - min: config.pool?.min || 0, - max: config.pool?.max || 10, - }, - }); - - return true; - } catch (error) { - logger.error('Failed to establish database connection', { - environment, - host: config.connection?.host, - database: config.connection?.database, - error: error.message, - stack: error.stack, - }); - throw error; - } } /** @@ -57,7 +57,7 @@ async function initializeDatabase() { * @returns {boolean} Connection status */ function isDbConnected() { - return isConnected; + return isConnected; } /** @@ -65,19 +65,19 @@ function isDbConnected() { * @returns {Promise} */ async function closeDatabase() { - try { - if (db && isConnected) { - await db.destroy(); - isConnected = false; - logger.info('Database connection closed'); + try { + if (db && isConnected) { + await db.destroy(); + isConnected = false; + logger.info('Database connection closed'); + } + } catch (error) { + logger.error('Error closing database connection:', error); + throw error; } - } catch (error) { - logger.error('Error closing database connection:', error); - throw error; - } } module.exports = db; module.exports.initializeDatabase = initializeDatabase; module.exports.isDbConnected = isDbConnected; -module.exports.closeDatabase = closeDatabase; +module.exports.closeDatabase = closeDatabase; \ No newline at end of file diff --git a/src/database/migrations/001_initial_system_tables.js b/src/database/migrations/001_initial_system_tables.js index 0d5f17f..55f897b 100644 --- a/src/database/migrations/001_initial_system_tables.js +++ b/src/database/migrations/001_initial_system_tables.js @@ -1,4 +1,4 @@ -exports.up = async function (knex) { +exports.up = async function(knex) { // System configuration with hot-reloading support await knex.schema.createTable('system_config', (table) => { table.increments('id').primary(); @@ -182,11 +182,11 @@ exports.up = async function (knex) { }); }; -exports.down = async function (knex) { +exports.down = async function(knex) { await knex.schema.dropTableIfExists('plugins'); await knex.schema.dropTableIfExists('event_instances'); await knex.schema.dropTableIfExists('event_types'); await knex.schema.dropTableIfExists('game_tick_log'); await knex.schema.dropTableIfExists('game_tick_config'); await knex.schema.dropTableIfExists('system_config'); -}; +}; \ No newline at end of file diff --git a/src/database/migrations/002_user_management.js b/src/database/migrations/002_user_management.js index 2bfb6eb..087622f 100644 --- a/src/database/migrations/002_user_management.js +++ b/src/database/migrations/002_user_management.js @@ -1,4 +1,4 @@ -exports.up = async function (knex) { +exports.up = async function(knex) { // Admin users with role-based access await knex.schema.createTable('admin_users', (table) => { table.increments('id').primary(); @@ -45,7 +45,7 @@ exports.up = async function (knex) { table.jsonb('setting_value').notNullable(); table.timestamp('created_at').defaultTo(knex.fn.now()); table.timestamp('updated_at').defaultTo(knex.fn.now()); - + table.unique(['player_id', 'setting_key']); }); @@ -83,9 +83,9 @@ exports.up = async function (knex) { }); }; -exports.down = async function (knex) { +exports.down = async function(knex) { await knex.schema.dropTableIfExists('player_subscriptions'); await knex.schema.dropTableIfExists('player_settings'); await knex.schema.dropTableIfExists('players'); await knex.schema.dropTableIfExists('admin_users'); -}; +}; \ No newline at end of file diff --git a/src/database/migrations/003_galaxy_colonies.js b/src/database/migrations/003_galaxy_colonies.js index 5caddac..0feb90b 100644 --- a/src/database/migrations/003_galaxy_colonies.js +++ b/src/database/migrations/003_galaxy_colonies.js @@ -1,4 +1,4 @@ -exports.up = async function (knex) { +exports.up = async function(knex) { // Planet types with generation rules await knex.schema.createTable('planet_types', (table) => { table.increments('id').primary(); @@ -248,10 +248,10 @@ exports.up = async function (knex) { ]); }; -exports.down = async function (knex) { +exports.down = async function(knex) { await knex.schema.dropTableIfExists('colony_buildings'); await knex.schema.dropTableIfExists('building_types'); await knex.schema.dropTableIfExists('colonies'); await knex.schema.dropTableIfExists('galaxy_sectors'); await knex.schema.dropTableIfExists('planet_types'); -}; +}; \ No newline at end of file diff --git a/src/database/migrations/004.5_missing_fleet_tables.js b/src/database/migrations/004.5_missing_fleet_tables.js deleted file mode 100644 index 69c0ab5..0000000 --- a/src/database/migrations/004.5_missing_fleet_tables.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Missing Fleet Tables Migration - * Adds fleet-related tables that were missing from previous migrations - */ - -exports.up = function (knex) { - return knex.schema - // Create fleets table - .createTable('fleets', (table) => { - table.increments('id').primary(); - table.integer('player_id').notNullable().references('players.id').onDelete('CASCADE'); - table.string('name', 100).notNullable(); - table.string('current_location', 20).notNullable(); // Coordinates - table.string('destination', 20).nullable(); // If moving - table.string('fleet_status', 20).defaultTo('idle') - .checkIn(['idle', 'moving', 'in_combat', 'constructing', 'repairing']); - table.timestamp('movement_started').nullable(); - table.timestamp('arrival_time').nullable(); - table.timestamp('last_updated').defaultTo(knex.fn.now()); - table.timestamp('created_at').defaultTo(knex.fn.now()); - - table.index(['player_id']); - table.index(['current_location']); - table.index(['fleet_status']); - table.index(['arrival_time']); - }) - - // Create ship_designs table - .createTable('ship_designs', (table) => { - table.increments('id').primary(); - table.integer('player_id').nullable().references('players.id').onDelete('CASCADE'); // NULL for public designs - table.string('name', 100).notNullable(); - table.string('ship_class', 50).notNullable(); // 'fighter', 'corvette', 'destroyer', 'cruiser', 'battleship' - table.string('hull_type', 50).notNullable(); - table.jsonb('components').notNullable(); // Weapon, shield, engine configurations - table.jsonb('stats').notNullable(); // Calculated stats: hp, attack, defense, speed, etc. - table.jsonb('cost').notNullable(); // Resource cost to build - table.integer('build_time').notNullable(); // In minutes - table.boolean('is_public').defaultTo(false); // Available to all players - table.boolean('is_active').defaultTo(true); - table.timestamp('created_at').defaultTo(knex.fn.now()); - table.timestamp('updated_at').defaultTo(knex.fn.now()); - - table.index(['player_id']); - table.index(['ship_class']); - table.index(['is_public']); - table.index(['is_active']); - }) - - // Create fleet_ships table - .createTable('fleet_ships', (table) => { - table.increments('id').primary(); - table.integer('fleet_id').notNullable().references('fleets.id').onDelete('CASCADE'); - table.integer('ship_design_id').notNullable().references('ship_designs.id').onDelete('CASCADE'); - table.integer('quantity').notNullable().defaultTo(1); - table.decimal('health_percentage', 5, 2).defaultTo(100.00); - table.integer('experience').defaultTo(0); - table.timestamp('created_at').defaultTo(knex.fn.now()); - - table.index(['fleet_id']); - table.index(['ship_design_id']); - }); -}; - -exports.down = function (knex) { - return knex.schema - .dropTableIfExists('fleet_ships') - .dropTableIfExists('ship_designs') - .dropTableIfExists('fleets'); -}; diff --git a/src/database/migrations/004_resources_economy.js b/src/database/migrations/004_resources_economy.js index 9ce04c5..393e87f 100644 --- a/src/database/migrations/004_resources_economy.js +++ b/src/database/migrations/004_resources_economy.js @@ -1,4 +1,4 @@ -exports.up = async function (knex) { +exports.up = async function(knex) { // Resource types await knex.schema.createTable('resource_types', (table) => { table.increments('id').primary(); @@ -85,9 +85,9 @@ exports.up = async function (knex) { ]); }; -exports.down = async function (knex) { +exports.down = async function(knex) { await knex.schema.dropTableIfExists('trade_routes'); await knex.schema.dropTableIfExists('colony_resource_production'); await knex.schema.dropTableIfExists('player_resources'); await knex.schema.dropTableIfExists('resource_types'); -}; +}; \ No newline at end of file diff --git a/src/database/migrations/005_minor_enhancements.js b/src/database/migrations/005_minor_enhancements.js deleted file mode 100644 index d268fb8..0000000 --- a/src/database/migrations/005_minor_enhancements.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Minor Schema Enhancements Migration - * Adds missing columns for player tick processing and research facilities - */ - -exports.up = async function (knex) { - // Check if columns exist before adding them - const hasLastTickProcessed = await knex.schema.hasColumn('players', 'last_tick_processed'); - const hasLastTickProcessedAt = await knex.schema.hasColumn('players', 'last_tick_processed_at'); - const hasLastCalculated = await knex.schema.hasColumn('colony_resource_production', 'last_calculated'); - const hasResearchFacilities = await knex.schema.hasTable('research_facilities'); - - let schema = knex.schema; - - // Add columns to players table if they don't exist - if (!hasLastTickProcessed || !hasLastTickProcessedAt) { - schema = schema.alterTable('players', (table) => { - if (!hasLastTickProcessed) { - table.bigInteger('last_tick_processed').nullable(); - } - if (!hasLastTickProcessedAt) { - table.timestamp('last_tick_processed_at').nullable(); - } - }); - } - - // Add last_calculated column to colony_resource_production if it doesn't exist - if (!hasLastCalculated) { - schema = schema.alterTable('colony_resource_production', (table) => { - table.timestamp('last_calculated').defaultTo(knex.fn.now()); - }); - } - - // Create research_facilities table if it doesn't exist - if (!hasResearchFacilities) { - schema = schema.createTable('research_facilities', (table) => { - table.increments('id').primary(); - table.integer('colony_id').notNullable().references('id').inTable('colonies').onDelete('CASCADE'); - table.string('name', 100).notNullable(); - table.string('facility_type', 50).notNullable(); - table.decimal('research_bonus', 3, 2).defaultTo(1.0); - table.jsonb('specialization').nullable(); - table.boolean('is_active').defaultTo(true); - table.timestamp('created_at').defaultTo(knex.fn.now()); - - table.index('colony_id'); - table.index('is_active'); - }); - } - - return schema; -}; - -exports.down = function (knex) { - return knex.schema - .dropTableIfExists('research_facilities') - .alterTable('colony_resource_production', (table) => { - table.dropColumn('last_calculated'); - }) - .alterTable('players', (table) => { - table.dropColumn('last_tick_processed'); - table.dropColumn('last_tick_processed_at'); - }); -}; diff --git a/src/database/migrations/006_combat_system_enhancement.js b/src/database/migrations/006_combat_system_enhancement.js deleted file mode 100644 index a5669c4..0000000 --- a/src/database/migrations/006_combat_system_enhancement.js +++ /dev/null @@ -1,292 +0,0 @@ -/** - * Combat System Enhancement Migration - * Adds comprehensive combat tables and enhancements for production-ready combat system - */ - -exports.up = function (knex) { - return knex.schema - // Combat types table - defines different combat resolution types - .createTable('combat_types', (table) => { - table.increments('id').primary(); - table.string('name', 100).unique().notNullable(); - table.text('description'); - table.string('plugin_name', 100); // References plugins table - table.jsonb('config'); - table.boolean('is_active').defaultTo(true); - - table.index(['is_active']); - table.index(['plugin_name']); - }) - - // Main battles table - tracks all combat encounters - .createTable('battles', (table) => { - table.bigIncrements('id').primary(); - table.string('battle_type', 50).notNullable(); // 'fleet_vs_fleet', 'fleet_vs_colony', 'siege' - table.string('location', 20).notNullable(); - table.integer('combat_type_id').references('combat_types.id'); - table.jsonb('participants').notNullable(); // Array of fleet/player IDs - table.string('status', 20).notNullable().defaultTo('pending'); // 'pending', 'active', 'completed', 'cancelled' - table.jsonb('battle_data'); // Additional battle configuration - table.jsonb('result'); // Final battle results - table.timestamp('started_at').defaultTo(knex.fn.now()); - table.timestamp('completed_at').nullable(); - table.timestamp('created_at').defaultTo(knex.fn.now()); - - table.index(['location']); - table.index(['status']); - table.index(['completed_at']); - table.index(['started_at']); - }) - - // Combat encounters table for detailed battle tracking - .createTable('combat_encounters', (table) => { - table.bigIncrements('id').primary(); - table.integer('battle_id').references('battles.id').onDelete('CASCADE'); - table.integer('attacker_fleet_id').references('fleets.id').onDelete('CASCADE').notNullable(); - table.integer('defender_fleet_id').references('fleets.id').onDelete('CASCADE'); - table.integer('defender_colony_id').references('colonies.id').onDelete('CASCADE'); - table.string('encounter_type', 50).notNullable(); // 'fleet_vs_fleet', 'fleet_vs_colony', 'siege' - table.string('location', 20).notNullable(); - table.jsonb('initial_forces').notNullable(); // Starting forces for both sides - table.jsonb('final_forces').notNullable(); // Remaining forces after combat - table.jsonb('casualties').notNullable(); // Detailed casualty breakdown - table.jsonb('combat_log').notNullable(); // Round-by-round combat log - table.decimal('experience_gained', 10, 2).defaultTo(0); - table.jsonb('loot_awarded'); // Resources/items awarded to winner - table.string('outcome', 20).notNullable(); // 'attacker_victory', 'defender_victory', 'draw' - table.integer('duration_seconds').notNullable(); // Combat duration - table.timestamp('started_at').notNullable(); - table.timestamp('completed_at').notNullable(); - table.timestamp('created_at').defaultTo(knex.fn.now()); - - table.index(['battle_id']); - table.index(['attacker_fleet_id']); - table.index(['defender_fleet_id']); - table.index(['defender_colony_id']); - table.index(['location']); - table.index(['outcome']); - table.index(['started_at']); - }) - - // Combat logs for detailed event tracking - .createTable('combat_logs', (table) => { - table.bigIncrements('id').primary(); - table.bigInteger('encounter_id').references('combat_encounters.id').onDelete('CASCADE').notNullable(); - table.integer('round_number').notNullable(); - table.string('event_type', 50).notNullable(); // 'damage', 'destruction', 'ability_use', 'experience_gain' - table.jsonb('event_data').notNullable(); // Detailed event information - table.timestamp('timestamp').defaultTo(knex.fn.now()); - - table.index(['encounter_id', 'round_number']); - table.index(['event_type']); - table.index(['timestamp']); - }) - - // Combat statistics for analysis and balancing - .createTable('combat_statistics', (table) => { - table.bigIncrements('id').primary(); - table.integer('player_id').references('players.id').onDelete('CASCADE').notNullable(); - table.integer('battles_initiated').defaultTo(0); - table.integer('battles_won').defaultTo(0); - table.integer('battles_lost').defaultTo(0); - table.integer('ships_lost').defaultTo(0); - table.integer('ships_destroyed').defaultTo(0); - table.bigInteger('total_damage_dealt').defaultTo(0); - table.bigInteger('total_damage_received').defaultTo(0); - table.decimal('total_experience_gained', 15, 2).defaultTo(0); - table.jsonb('resources_looted').defaultTo('{}'); - table.timestamp('last_battle').nullable(); - table.timestamp('created_at').defaultTo(knex.fn.now()); - table.timestamp('updated_at').defaultTo(knex.fn.now()); - - table.index(['player_id']); - table.index(['battles_won']); - table.index(['last_battle']); - }) - - // Ship combat experience and veterancy - .createTable('ship_combat_experience', (table) => { - table.bigIncrements('id').primary(); - table.integer('fleet_id').references('fleets.id').onDelete('CASCADE').notNullable(); - table.integer('ship_design_id').references('ship_designs.id').onDelete('CASCADE').notNullable(); - table.integer('battles_survived').defaultTo(0); - table.integer('enemies_destroyed').defaultTo(0); - table.bigInteger('damage_dealt').defaultTo(0); - table.decimal('experience_points', 15, 2).defaultTo(0); - table.integer('veterancy_level').defaultTo(1); - table.jsonb('combat_bonuses').defaultTo('{}'); // Experience-based bonuses - table.timestamp('last_combat').nullable(); - table.timestamp('created_at').defaultTo(knex.fn.now()); - table.timestamp('updated_at').defaultTo(knex.fn.now()); - - table.unique(['fleet_id', 'ship_design_id']); - table.index(['fleet_id']); - table.index(['veterancy_level']); - table.index(['last_combat']); - }) - - // Combat configurations for different combat types - .createTable('combat_configurations', (table) => { - table.increments('id').primary(); - table.string('config_name', 100).unique().notNullable(); - table.string('combat_type', 50).notNullable(); // 'instant', 'turn_based', 'real_time' - table.jsonb('config_data').notNullable(); // Combat-specific configuration - table.boolean('is_active').defaultTo(true); - table.string('description', 500); - table.timestamp('created_at').defaultTo(knex.fn.now()); - table.timestamp('updated_at').defaultTo(knex.fn.now()); - - table.index(['combat_type']); - table.index(['is_active']); - }) - - // Combat modifiers for temporary effects - .createTable('combat_modifiers', (table) => { - table.bigIncrements('id').primary(); - table.string('entity_type', 50).notNullable(); // 'fleet', 'colony', 'player' - table.integer('entity_id').notNullable(); - table.string('modifier_type', 50).notNullable(); // 'attack_bonus', 'defense_bonus', 'speed_bonus' - table.decimal('modifier_value', 8, 4).notNullable(); - table.string('source', 100).notNullable(); // 'technology', 'event', 'building', 'experience' - table.timestamp('start_time').defaultTo(knex.fn.now()); - table.timestamp('end_time').nullable(); - table.boolean('is_active').defaultTo(true); - table.jsonb('metadata'); // Additional modifier information - - table.index(['entity_type', 'entity_id']); - table.index(['modifier_type']); - table.index(['is_active']); - table.index(['end_time']); - }) - - // Fleet positioning for tactical combat - .createTable('fleet_positions', (table) => { - table.bigIncrements('id').primary(); - table.integer('fleet_id').references('fleets.id').onDelete('CASCADE').notNullable(); - table.string('location', 20).notNullable(); - table.decimal('position_x', 8, 2).defaultTo(0); - table.decimal('position_y', 8, 2).defaultTo(0); - table.decimal('position_z', 8, 2).defaultTo(0); - table.string('formation', 50).defaultTo('standard'); // 'standard', 'defensive', 'aggressive', 'flanking' - table.jsonb('tactical_settings').defaultTo('{}'); // Formation-specific settings - table.timestamp('last_updated').defaultTo(knex.fn.now()); - - table.unique(['fleet_id']); - table.index(['location']); - table.index(['formation']); - }) - - // Combat queue for processing battles - .createTable('combat_queue', (table) => { - table.bigIncrements('id').primary(); - table.bigInteger('battle_id').references('battles.id').onDelete('CASCADE').notNullable(); - table.string('queue_status', 20).defaultTo('pending'); // 'pending', 'processing', 'completed', 'failed' - table.integer('priority').defaultTo(100); - table.timestamp('scheduled_at').defaultTo(knex.fn.now()); - table.timestamp('started_processing').nullable(); - table.timestamp('completed_at').nullable(); - table.integer('retry_count').defaultTo(0); - table.text('error_message').nullable(); - table.jsonb('processing_metadata'); - - table.index(['queue_status']); - table.index(['priority', 'scheduled_at']); - table.index(['battle_id']); - }) - - // Extend battles table with additional fields - .alterTable('battles', (table) => { - table.integer('combat_configuration_id').references('combat_configurations.id'); - table.jsonb('tactical_settings').defaultTo('{}'); - table.integer('spectator_count').defaultTo(0); - table.jsonb('environmental_effects'); // Weather, nebulae, asteroid fields - table.decimal('estimated_duration', 8, 2); // Estimated battle duration in seconds - }) - - // Extend fleets table with combat-specific fields - .alterTable('fleets', (table) => { - table.decimal('combat_rating', 10, 2).defaultTo(0); // Calculated combat effectiveness - table.integer('total_ship_count').defaultTo(0); - table.jsonb('fleet_composition').defaultTo('{}'); // Ship type breakdown - table.timestamp('last_combat').nullable(); - table.integer('combat_victories').defaultTo(0); - table.integer('combat_defeats').defaultTo(0); - }) - - // Extend ship_designs table with detailed combat stats - .alterTable('ship_designs', (table) => { - table.integer('hull_points').defaultTo(100); - table.integer('shield_points').defaultTo(0); - table.integer('armor_points').defaultTo(0); - table.decimal('attack_power', 8, 2).defaultTo(10); - table.decimal('attack_speed', 6, 2).defaultTo(1.0); // Attacks per second - table.decimal('movement_speed', 6, 2).defaultTo(1.0); - table.integer('cargo_capacity').defaultTo(0); - table.jsonb('special_abilities').defaultTo('[]'); - table.jsonb('damage_resistances').defaultTo('{}'); - }) - - // Colony defense enhancements - .alterTable('colonies', (table) => { - table.integer('defense_rating').defaultTo(0); - table.integer('shield_strength').defaultTo(0); - table.boolean('under_siege').defaultTo(false); - table.timestamp('last_attacked').nullable(); - table.integer('successful_defenses').defaultTo(0); - table.integer('times_captured').defaultTo(0); - }); -}; - -exports.down = function (knex) { - return knex.schema - // Remove added columns first - .alterTable('colonies', (table) => { - table.dropColumn('defense_rating'); - table.dropColumn('shield_strength'); - table.dropColumn('under_siege'); - table.dropColumn('last_attacked'); - table.dropColumn('successful_defenses'); - table.dropColumn('times_captured'); - }) - - .alterTable('ship_designs', (table) => { - table.dropColumn('hull_points'); - table.dropColumn('shield_points'); - table.dropColumn('armor_points'); - table.dropColumn('attack_power'); - table.dropColumn('attack_speed'); - table.dropColumn('movement_speed'); - table.dropColumn('cargo_capacity'); - table.dropColumn('special_abilities'); - table.dropColumn('damage_resistances'); - }) - - .alterTable('fleets', (table) => { - table.dropColumn('combat_rating'); - table.dropColumn('total_ship_count'); - table.dropColumn('fleet_composition'); - table.dropColumn('last_combat'); - table.dropColumn('combat_victories'); - table.dropColumn('combat_defeats'); - }) - - .alterTable('battles', (table) => { - table.dropColumn('combat_configuration_id'); - table.dropColumn('tactical_settings'); - table.dropColumn('spectator_count'); - table.dropColumn('environmental_effects'); - table.dropColumn('estimated_duration'); - }) - - // Drop new tables - .dropTableIfExists('combat_queue') - .dropTableIfExists('fleet_positions') - .dropTableIfExists('combat_modifiers') - .dropTableIfExists('combat_configurations') - .dropTableIfExists('ship_combat_experience') - .dropTableIfExists('combat_statistics') - .dropTableIfExists('combat_logs') - .dropTableIfExists('combat_encounters') - .dropTableIfExists('battles') - .dropTableIfExists('combat_types'); -}; diff --git a/src/database/migrations/007_research_system.js b/src/database/migrations/007_research_system.js deleted file mode 100644 index 61574e1..0000000 --- a/src/database/migrations/007_research_system.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Research System Migration - * Creates tables for the technology tree and research system - */ - -exports.up = async function(knex) { - console.log('Creating research system tables...'); - - // Technology tree table - await knex.schema.createTable('technologies', (table) => { - table.increments('id').primary(); - table.string('name', 100).unique().notNullable(); - table.text('description'); - table.string('category', 50).notNullable(); // 'military', 'industrial', 'social', 'exploration' - table.integer('tier').notNullable().defaultTo(1); - table.jsonb('prerequisites'); // Array of required technology IDs - table.jsonb('research_cost').notNullable(); // Resource costs - table.integer('research_time').notNullable(); // In minutes - table.jsonb('effects'); // Bonuses, unlocks, etc. - table.boolean('is_active').defaultTo(true); - table.timestamp('created_at').defaultTo(knex.fn.now()); - - table.index(['category']); - table.index(['tier']); - table.index(['is_active']); - }); - - // Player research progress table - await knex.schema.createTable('player_research', (table) => { - table.increments('id').primary(); - table.integer('player_id').notNullable().references('id').inTable('players').onDelete('CASCADE'); - table.integer('technology_id').notNullable().references('id').inTable('technologies'); - table.string('status', 20).defaultTo('available').checkIn(['unavailable', 'available', 'researching', 'completed']); - table.integer('progress').defaultTo(0); - table.timestamp('started_at'); - table.timestamp('completed_at'); - table.unique(['player_id', 'technology_id']); - - table.index(['player_id']); - table.index(['status']); - table.index(['player_id', 'status']); - }); - - // Research facilities table (already exists but let's ensure it has proper constraints) - const hasResearchFacilities = await knex.schema.hasTable('research_facilities'); - if (!hasResearchFacilities) { - await knex.schema.createTable('research_facilities', (table) => { - table.increments('id').primary(); - table.integer('colony_id').notNullable().references('id').inTable('colonies').onDelete('CASCADE'); - table.string('name', 100).notNullable(); - table.string('facility_type', 50).notNullable(); - table.decimal('research_bonus', 3, 2).defaultTo(1.0); // Multiplier for research speed - table.jsonb('specialization'); // Categories this facility is good at - table.boolean('is_active').defaultTo(true); - table.timestamp('created_at').defaultTo(knex.fn.now()); - - table.index(['colony_id']); - table.index(['is_active']); - }); - } - - // Add missing indexes to existing tables if they don't exist - const hasPlayerResourcesIndex = await knex.schema.hasTable('player_resources'); - if (hasPlayerResourcesIndex) { - // Check if index exists before creating - try { - await knex.schema.table('player_resources', (table) => { - table.index(['player_id'], 'idx_player_resources_player_id'); - }); - } catch (e) { - // Index likely already exists, ignore - console.log('Player resources index already exists or error creating it'); - } - } - - console.log('Research system tables created successfully'); -}; - -exports.down = async function(knex) { - await knex.schema.dropTableIfExists('player_research'); - await knex.schema.dropTableIfExists('technologies'); - // Don't drop research_facilities as it might be used by other systems -}; \ No newline at end of file diff --git a/src/database/seeds/001_initial_data.js b/src/database/seeds/001_initial_data.js index ec4c4f8..423a9be 100644 --- a/src/database/seeds/001_initial_data.js +++ b/src/database/seeds/001_initial_data.js @@ -3,25 +3,15 @@ * Populates essential game data for development and testing */ -exports.seed = async function (knex) { +exports.seed = async function(knex) { console.log('Seeding initial game data...'); // Clear existing data (be careful in production!) if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { - // Only clear tables that exist in our current schema - try { - await knex('admin_users').del(); - console.log('✓ Cleared admin_users'); - } catch (e) { - console.log('! admin_users table does not exist, skipping...'); - } - - try { - await knex('building_types').del(); - console.log('✓ Cleared building_types'); - } catch (e) { - console.log('! building_types table does not exist, skipping...'); - } + await knex('admin_users').del(); + await knex('building_types').del(); + await knex('ship_categories').del(); + await knex('research_technologies').del(); } // Insert default admin user @@ -41,12 +31,8 @@ exports.seed = async function (knex) { }, ]; - try { - await knex('admin_users').insert(adminUsers); - console.log('✓ Admin users seeded'); - } catch (e) { - console.log('! Could not seed admin_users:', e.message); - } + await knex('admin_users').insert(adminUsers); + console.log('✓ Admin users seeded'); // Insert building types const buildingTypes = [ @@ -132,16 +118,199 @@ exports.seed = async function (knex) { }, ]; - try { - await knex('building_types').insert(buildingTypes); - console.log('✓ Building types seeded'); - } catch (e) { - console.log('! Could not seed building_types:', e.message); + await knex('building_types').insert(buildingTypes); + console.log('✓ Building types seeded'); + + // Insert building effects + const buildingEffects = [ + // Scrap Processor production + { building_type_id: 2, effect_type: 'production', resource_type: 'scrap', base_value: 50, scaling_per_level: 25 }, + // Energy Generator production + { building_type_id: 3, effect_type: 'production', resource_type: 'energy', base_value: 30, scaling_per_level: 15 }, + // Data Archive production + { building_type_id: 4, effect_type: 'production', resource_type: 'data_cores', base_value: 5, scaling_per_level: 3 }, + // Mining Complex production + { building_type_id: 5, effect_type: 'production', resource_type: 'rare_elements', base_value: 2, scaling_per_level: 1 }, + ]; + + await knex('building_effects').insert(buildingEffects); + console.log('✓ Building effects seeded'); + + // Insert ship categories + const shipCategories = [ + { + name: 'Scout', + description: 'Fast, lightly armed reconnaissance vessel', + base_hull_points: 50, + base_speed: 20, + base_cargo_capacity: 10, + module_slots_light: 3, + module_slots_medium: 1, + module_slots_heavy: 0, + }, + { + name: 'Frigate', + description: 'Balanced combat vessel with moderate capabilities', + base_hull_points: 150, + base_speed: 15, + base_cargo_capacity: 25, + module_slots_light: 4, + module_slots_medium: 2, + module_slots_heavy: 1, + }, + { + name: 'Destroyer', + description: 'Heavy combat vessel with powerful weapons', + base_hull_points: 300, + base_speed: 10, + base_cargo_capacity: 15, + module_slots_light: 2, + module_slots_medium: 4, + module_slots_heavy: 2, + }, + { + name: 'Transport', + description: 'Large cargo vessel with minimal combat capability', + base_hull_points: 100, + base_speed: 8, + base_cargo_capacity: 100, + module_slots_light: 2, + module_slots_medium: 1, + module_slots_heavy: 0, + }, + ]; + + await knex('ship_categories').insert(shipCategories); + console.log('✓ Ship categories seeded'); + + // Insert research technologies + const technologies = [ + { + category_id: 1, // engineering + name: 'Advanced Materials', + description: 'Improved construction materials for stronger buildings', + level: 1, + base_research_cost: 100, + base_research_time_hours: 4, + prerequisites: JSON.stringify([]), + effects: JSON.stringify({ building_cost_reduction: 0.1 }), + }, + { + category_id: 2, // physics + name: 'Fusion Power', + description: 'More efficient energy generation technology', + level: 1, + base_research_cost: 150, + base_research_time_hours: 6, + prerequisites: JSON.stringify([]), + effects: JSON.stringify({ energy_production_bonus: 0.25 }), + }, + { + category_id: 3, // computing + name: 'Data Mining', + description: 'Advanced algorithms for information processing', + level: 1, + base_research_cost: 200, + base_research_time_hours: 8, + prerequisites: JSON.stringify([]), + effects: JSON.stringify({ data_core_production_bonus: 0.2 }), + }, + { + category_id: 4, // military + name: 'Weapon Systems', + description: 'Basic military technology for ship weapons', + level: 1, + base_research_cost: 250, + base_research_time_hours: 10, + prerequisites: JSON.stringify([]), + effects: JSON.stringify({ combat_rating_bonus: 0.15 }), + }, + ]; + + await knex('research_technologies').insert(technologies); + console.log('✓ Research technologies seeded'); + + // Insert some test sectors and systems for development + if (process.env.NODE_ENV === 'development') { + const sectors = [ + { + name: 'Sol Sector', + description: 'The remnants of humanity\'s birthplace', + x_coordinate: 0, + y_coordinate: 0, + sector_type: 'starting', + danger_level: 1, + resource_modifier: 1.0, + }, + { + name: 'Alpha Centauri Sector', + description: 'First expansion zone with moderate resources', + x_coordinate: 1, + y_coordinate: 0, + sector_type: 'normal', + danger_level: 2, + resource_modifier: 1.1, + }, + ]; + + await knex('sectors').insert(sectors); + + const systems = [ + { + sector_id: 1, + name: 'Sol System', + x_coordinate: 0, + y_coordinate: 0, + star_type: 'main_sequence', + system_size: 8, + is_explored: true, + }, + { + sector_id: 2, + name: 'Alpha Centauri A', + x_coordinate: 0, + y_coordinate: 0, + star_type: 'main_sequence', + system_size: 5, + is_explored: false, + }, + ]; + + await knex('star_systems').insert(systems); + + const planets = [ + { + system_id: 1, + name: 'Earth', + position: 3, + planet_type_id: 1, // terran + size: 150, + coordinates: 'SOL-03-E', + is_habitable: true, + }, + { + system_id: 1, + name: 'Mars', + position: 4, + planet_type_id: 2, // desert + size: 80, + coordinates: 'SOL-04-M', + is_habitable: true, + }, + { + system_id: 2, + name: 'Proxima b', + position: 1, + planet_type_id: 1, // terran + size: 120, + coordinates: 'ACA-01-P', + is_habitable: true, + }, + ]; + + await knex('planets').insert(planets); + console.log('✓ Test galaxy data seeded'); } - // Try to seed other tables if they exist - skip if they don't - console.log('Note: Skipping other seed data for tables that may not exist in current schema.'); - console.log('This is normal for the research system implementation phase.'); - console.log('Initial data seeding completed successfully!'); -}; +}; \ No newline at end of file diff --git a/src/database/seeds/002_technologies.js b/src/database/seeds/002_technologies.js deleted file mode 100644 index 3b3cc9a..0000000 --- a/src/database/seeds/002_technologies.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Technology Seeds - * Populates the technologies table with initial technology tree data - */ - -const { TECHNOLOGIES } = require('../../data/technologies'); - -/** - * Seed technologies table - */ -exports.seed = async function(knex) { - try { - console.log('Seeding technologies table...'); - - // Delete all existing entries (for development/testing) - // In production, you might want to handle this differently - await knex('technologies').del(); - - // Insert technology data - const technologiesToInsert = TECHNOLOGIES.map(tech => ({ - id: tech.id, - name: tech.name, - description: tech.description, - category: tech.category, - tier: tech.tier, - prerequisites: JSON.stringify(tech.prerequisites), - research_cost: JSON.stringify(tech.research_cost), - research_time: tech.research_time, - effects: JSON.stringify(tech.effects), - is_active: true, - created_at: new Date() - })); - - // Insert in batches to handle large datasets efficiently - const batchSize = 50; - for (let i = 0; i < technologiesToInsert.length; i += batchSize) { - const batch = technologiesToInsert.slice(i, i + batchSize); - await knex('technologies').insert(batch); - } - - console.log(`Successfully seeded ${technologiesToInsert.length} technologies`); - - // Verify the seeding - const count = await knex('technologies').count('* as count').first(); - console.log(`Total technologies in database: ${count.count}`); - - // Log technology counts by category and tier - const categoryStats = await knex('technologies') - .select('category') - .count('* as count') - .groupBy('category'); - - console.log('Technologies by category:'); - categoryStats.forEach(stat => { - console.log(` ${stat.category}: ${stat.count}`); - }); - - const tierStats = await knex('technologies') - .select('tier') - .count('* as count') - .groupBy('tier') - .orderBy('tier'); - - console.log('Technologies by tier:'); - tierStats.forEach(stat => { - console.log(` Tier ${stat.tier}: ${stat.count}`); - }); - - } catch (error) { - console.error('Error seeding technologies:', error); - throw error; - } -}; \ No newline at end of file diff --git a/src/middleware/admin.middleware.js b/src/middleware/admin.middleware.js index 73e4f00..aac1b95 100644 --- a/src/middleware/admin.middleware.js +++ b/src/middleware/admin.middleware.js @@ -13,84 +13,84 @@ const logger = require('../utils/logger'); * @param {Function} next - Express next function */ async function authenticateAdmin(req, res, next) { - try { - const correlationId = req.correlationId; + try { + const correlationId = req.correlationId; + + // Extract token from Authorization header + const authHeader = req.get('Authorization'); + const token = extractTokenFromHeader(authHeader); - // Extract token from Authorization header - const authHeader = req.get('Authorization'); - const token = extractTokenFromHeader(authHeader); + if (!token) { + logger.warn('Admin authentication failed - no token provided', { + correlationId, + ip: req.ip, + userAgent: req.get('User-Agent'), + path: req.path + }); - if (!token) { - logger.warn('Admin authentication failed - no token provided', { - correlationId, - ip: req.ip, - userAgent: req.get('User-Agent'), - path: req.path, - }); + return res.status(401).json({ + error: 'Authentication required', + message: 'No authentication token provided', + correlationId + }); + } - return res.status(401).json({ - error: 'Authentication required', - message: 'No authentication token provided', - correlationId, - }); + // Verify the token + const decoded = verifyAdminToken(token); + + // Add admin information to request object + req.user = { + adminId: decoded.adminId, + email: decoded.email, + username: decoded.username, + permissions: decoded.permissions || [], + type: 'admin', + iat: decoded.iat, + exp: decoded.exp + }; + + // Log admin access + logger.audit('Admin authenticated', { + correlationId, + adminId: decoded.adminId, + username: decoded.username, + permissions: decoded.permissions, + path: req.path, + method: req.method, + ip: req.ip, + userAgent: req.get('User-Agent') + }); + + next(); + + } catch (error) { + const correlationId = req.correlationId; + + logger.warn('Admin authentication failed', { + correlationId, + error: error.message, + ip: req.ip, + userAgent: req.get('User-Agent'), + path: req.path + }); + + let statusCode = 401; + let message = 'Invalid authentication token'; + + if (error.message === 'Token expired') { + statusCode = 401; + message = 'Authentication token has expired'; + } else if (error.message === 'Invalid token') { + statusCode = 401; + message = 'Invalid authentication token'; + } + + return res.status(statusCode).json({ + error: 'Authentication failed', + message, + correlationId + }); } - - // Verify the token - const decoded = verifyAdminToken(token); - - // Add admin information to request object - req.user = { - adminId: decoded.adminId, - email: decoded.email, - username: decoded.username, - permissions: decoded.permissions || [], - type: 'admin', - iat: decoded.iat, - exp: decoded.exp, - }; - - // Log admin access - logger.audit('Admin authenticated', { - correlationId, - adminId: decoded.adminId, - username: decoded.username, - permissions: decoded.permissions, - path: req.path, - method: req.method, - ip: req.ip, - userAgent: req.get('User-Agent'), - }); - - next(); - - } catch (error) { - const correlationId = req.correlationId; - - logger.warn('Admin authentication failed', { - correlationId, - error: error.message, - ip: req.ip, - userAgent: req.get('User-Agent'), - path: req.path, - }); - - let statusCode = 401; - let message = 'Invalid authentication token'; - - if (error.message === 'Token expired') { - statusCode = 401; - message = 'Authentication token has expired'; - } else if (error.message === 'Invalid token') { - statusCode = 401; - message = 'Invalid authentication token'; - } - - return res.status(statusCode).json({ - error: 'Authentication failed', - message, - correlationId, - }); - } } /** @@ -99,99 +99,99 @@ async function authenticateAdmin(req, res, next) { * @returns {Function} Express middleware function */ function requirePermissions(requiredPermissions) { - // Normalize to array - const permissions = Array.isArray(requiredPermissions) - ? requiredPermissions - : [requiredPermissions]; + // Normalize to array + const permissions = Array.isArray(requiredPermissions) + ? requiredPermissions + : [requiredPermissions]; - return (req, res, next) => { - try { - const correlationId = req.correlationId; - const adminPermissions = req.user?.permissions || []; - const adminId = req.user?.adminId; - const username = req.user?.username; + return (req, res, next) => { + try { + const correlationId = req.correlationId; + const adminPermissions = req.user?.permissions || []; + const adminId = req.user?.adminId; + const username = req.user?.username; - if (!adminId) { - logger.warn('Permission check failed - no authenticated admin', { - correlationId, - requiredPermissions: permissions, - path: req.path, - }); + if (!adminId) { + logger.warn('Permission check failed - no authenticated admin', { + correlationId, + requiredPermissions: permissions, + path: req.path + }); - return res.status(401).json({ - error: 'Authentication required', - message: 'Admin authentication required', - correlationId, - }); - } + return res.status(401).json({ + error: 'Authentication required', + message: 'Admin authentication required', + correlationId + }); + } - // Check if admin has super admin permission (bypasses all checks) - if (adminPermissions.includes('super_admin')) { - logger.info('Permission check passed - super admin', { - correlationId, - adminId, - username, - requiredPermissions: permissions, - path: req.path, - }); + // Check if admin has super admin permission (bypasses all checks) + if (adminPermissions.includes('super_admin')) { + logger.info('Permission check passed - super admin', { + correlationId, + adminId, + username, + requiredPermissions: permissions, + path: req.path + }); - return next(); - } + return next(); + } - // Check if admin has all required permissions - const hasPermissions = permissions.every(permission => - adminPermissions.includes(permission), - ); + // Check if admin has all required permissions + const hasPermissions = permissions.every(permission => + adminPermissions.includes(permission) + ); - if (!hasPermissions) { - const missingPermissions = permissions.filter(permission => - !adminPermissions.includes(permission), - ); + if (!hasPermissions) { + const missingPermissions = permissions.filter(permission => + !adminPermissions.includes(permission) + ); - logger.warn('Permission check failed - insufficient permissions', { - correlationId, - adminId, - username, - adminPermissions, - requiredPermissions: permissions, - missingPermissions, - path: req.path, - method: req.method, - }); + logger.warn('Permission check failed - insufficient permissions', { + correlationId, + adminId, + username, + adminPermissions, + requiredPermissions: permissions, + missingPermissions, + path: req.path, + method: req.method + }); - return res.status(403).json({ - error: 'Insufficient permissions', - message: 'You do not have the required permissions to access this resource', - requiredPermissions: permissions, - correlationId, - }); - } + return res.status(403).json({ + error: 'Insufficient permissions', + message: 'You do not have the required permissions to access this resource', + requiredPermissions: permissions, + correlationId + }); + } - logger.info('Permission check passed', { - correlationId, - adminId, - username, - requiredPermissions: permissions, - path: req.path, - }); + logger.info('Permission check passed', { + correlationId, + adminId, + username, + requiredPermissions: permissions, + path: req.path + }); - next(); + next(); - } catch (error) { - logger.error('Permission check error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - requiredPermissions: permissions, - }); + } catch (error) { + logger.error('Permission check error', { + correlationId: req.correlationId, + error: error.message, + stack: error.stack, + requiredPermissions: permissions + }); - return res.status(500).json({ - error: 'Internal server error', - message: 'Failed to verify permissions', - correlationId: req.correlationId, - }); - } - }; + return res.status(500).json({ + error: 'Internal server error', + message: 'Failed to verify permissions', + correlationId: req.correlationId + }); + } + }; } /** @@ -201,80 +201,80 @@ function requirePermissions(requiredPermissions) { * @returns {Function} Express middleware function */ function requirePlayerAccess(paramName = 'playerId') { - return (req, res, next) => { - try { - const correlationId = req.correlationId; - const adminPermissions = req.user?.permissions || []; - const adminId = req.user?.adminId; - const username = req.user?.username; - const targetPlayerId = req.params[paramName]; + return (req, res, next) => { + try { + const correlationId = req.correlationId; + const adminPermissions = req.user?.permissions || []; + const adminId = req.user?.adminId; + const username = req.user?.username; + const targetPlayerId = req.params[paramName]; - if (!adminId) { - return res.status(401).json({ - error: 'Authentication required', - correlationId, - }); - } + if (!adminId) { + return res.status(401).json({ + error: 'Authentication required', + correlationId + }); + } - // Super admin can access everything - if (adminPermissions.includes('super_admin')) { - return next(); - } + // Super admin can access everything + if (adminPermissions.includes('super_admin')) { + return next(); + } - // Check for player management permission - if (adminPermissions.includes('player_management')) { - logger.info('Player access granted - player management permission', { - correlationId, - adminId, - username, - targetPlayerId, - path: req.path, - }); - return next(); - } + // Check for player management permission + if (adminPermissions.includes('player_management')) { + logger.info('Player access granted - player management permission', { + correlationId, + adminId, + username, + targetPlayerId, + path: req.path + }); + return next(); + } - // Check for read-only player data permission for GET requests - if (req.method === 'GET' && adminPermissions.includes('player_data_read')) { - logger.info('Player access granted - read-only permission', { - correlationId, - adminId, - username, - targetPlayerId, - path: req.path, - }); - return next(); - } + // Check for read-only player data permission for GET requests + if (req.method === 'GET' && adminPermissions.includes('player_data_read')) { + logger.info('Player access granted - read-only permission', { + correlationId, + adminId, + username, + targetPlayerId, + path: req.path + }); + return next(); + } - logger.warn('Player access denied - insufficient permissions', { - correlationId, - adminId, - username, - adminPermissions, - targetPlayerId, - path: req.path, - method: req.method, - }); + logger.warn('Player access denied - insufficient permissions', { + correlationId, + adminId, + username, + adminPermissions, + targetPlayerId, + path: req.path, + method: req.method + }); - return res.status(403).json({ - error: 'Insufficient permissions', - message: 'You do not have permission to access player data', - correlationId, - }); + return res.status(403).json({ + error: 'Insufficient permissions', + message: 'You do not have permission to access player data', + correlationId + }); - } catch (error) { - logger.error('Player access check error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); + } catch (error) { + logger.error('Player access check error', { + correlationId: req.correlationId, + error: error.message, + stack: error.stack + }); - return res.status(500).json({ - error: 'Internal server error', - message: 'Failed to verify player access permissions', - correlationId: req.correlationId, - }); - } - }; + return res.status(500).json({ + error: 'Internal server error', + message: 'Failed to verify player access permissions', + correlationId: req.correlationId + }); + } + }; } /** @@ -283,77 +283,77 @@ function requirePlayerAccess(paramName = 'playerId') { * @returns {Function} Express middleware function */ function auditAdminAction(action) { - return (req, res, next) => { - try { - const correlationId = req.correlationId; - const adminId = req.user?.adminId; - const username = req.user?.username; + return (req, res, next) => { + try { + const correlationId = req.correlationId; + const adminId = req.user?.adminId; + const username = req.user?.username; - // Log the action - logger.audit('Admin action initiated', { - correlationId, - adminId, - username, - action, - path: req.path, - method: req.method, - params: req.params, - query: req.query, - ip: req.ip, - userAgent: req.get('User-Agent'), - }); + // Log the action + logger.audit('Admin action initiated', { + correlationId, + adminId, + username, + action, + path: req.path, + method: req.method, + params: req.params, + query: req.query, + ip: req.ip, + userAgent: req.get('User-Agent') + }); - // Override res.json to log the response - const originalJson = res.json; - res.json = function (data) { - logger.audit('Admin action completed', { - correlationId, - adminId, - username, - action, - path: req.path, - method: req.method, - statusCode: res.statusCode, - success: res.statusCode < 400, - }); + // Override res.json to log the response + const originalJson = res.json; + res.json = function(data) { + logger.audit('Admin action completed', { + correlationId, + adminId, + username, + action, + path: req.path, + method: req.method, + statusCode: res.statusCode, + success: res.statusCode < 400 + }); - return originalJson.call(this, data); - }; + return originalJson.call(this, data); + }; - next(); + next(); - } catch (error) { - logger.error('Admin audit logging error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - action, - }); + } catch (error) { + logger.error('Admin audit logging error', { + correlationId: req.correlationId, + error: error.message, + stack: error.stack, + action + }); - // Continue even if audit logging fails - next(); - } - }; + // Continue even if audit logging fails + next(); + } + }; } /** * Common admin permission constants */ const ADMIN_PERMISSIONS = { - SUPER_ADMIN: 'super_admin', - PLAYER_MANAGEMENT: 'player_management', - PLAYER_DATA_READ: 'player_data_read', - SYSTEM_MANAGEMENT: 'system_management', - GAME_MANAGEMENT: 'game_management', - EVENT_MANAGEMENT: 'event_management', - ANALYTICS_READ: 'analytics_read', - CONTENT_MANAGEMENT: 'content_management', + SUPER_ADMIN: 'super_admin', + PLAYER_MANAGEMENT: 'player_management', + PLAYER_DATA_READ: 'player_data_read', + SYSTEM_MANAGEMENT: 'system_management', + GAME_MANAGEMENT: 'game_management', + EVENT_MANAGEMENT: 'event_management', + ANALYTICS_READ: 'analytics_read', + CONTENT_MANAGEMENT: 'content_management' }; module.exports = { - authenticateAdmin, - requirePermissions, - requirePlayerAccess, - auditAdminAction, - ADMIN_PERMISSIONS, -}; + authenticateAdmin, + requirePermissions, + requirePlayerAccess, + auditAdminAction, + ADMIN_PERMISSIONS +}; \ No newline at end of file diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 02875b4..c854103 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -25,8 +25,8 @@ function authenticateToken(userType = 'player') { try { // Verify token - const decoded = jwt.verify(token, (process.env.JWT_PLAYER_SECRET || "player-secret-change-in-production")); - + const decoded = jwt.verify(token, process.env.JWT_SECRET); + // Check token type matches required type if (decoded.type !== userType) { return res.status(403).json({ @@ -38,7 +38,7 @@ function authenticateToken(userType = 'player') { // Get user from database const tableName = userType === 'admin' ? 'admin_users' : 'players'; const user = await db(tableName) - .where('id', decoded.playerId) + .where('id', decoded.userId) .first(); if (!user) { @@ -49,11 +49,11 @@ function authenticateToken(userType = 'player') { } // Check if user is active - if (userType === 'player' && !user.is_active) { + if (userType === 'player' && user.account_status !== 'active') { return res.status(403).json({ error: 'Account is not active', code: 'ACCOUNT_INACTIVE', - status: user.is_active ? "active" : "inactive", + status: user.account_status, }); } @@ -117,15 +117,15 @@ function optionalAuth(userType = 'player') { } try { - const decoded = jwt.verify(token, (process.env.JWT_PLAYER_SECRET || "player-secret-change-in-production")); - + const decoded = jwt.verify(token, process.env.JWT_SECRET); + if (decoded.type === userType) { const tableName = userType === 'admin' ? 'admin_users' : 'players'; const user = await db(tableName) - .where('id', decoded.playerId) + .where('id', decoded.userId) .first(); - if (user && ((userType === 'player' && user.is_active) || + if (user && ((userType === 'player' && user.account_status === 'active') || (userType === 'admin' && user.is_active))) { req.user = user; req.token = decoded; @@ -180,7 +180,7 @@ function requirePermission(permission) { */ function requireRole(roles) { const requiredRoles = Array.isArray(roles) ? roles : [roles]; - + return (req, res, next) => { if (!req.user) { return res.status(401).json({ @@ -207,4 +207,4 @@ module.exports = { optionalAuth, requirePermission, requireRole, -}; +}; \ No newline at end of file diff --git a/src/middleware/auth.js.backup b/src/middleware/auth.js.backup deleted file mode 100644 index 7da4340..0000000 --- a/src/middleware/auth.js.backup +++ /dev/null @@ -1,210 +0,0 @@ -/** - * Authentication middleware for JWT token validation - */ - -const jwt = require('jsonwebtoken'); -const logger = require('../utils/logger'); -const db = require('../database/connection'); - -/** - * Verify JWT token and attach user to request - * @param {string} userType - 'player' or 'admin' - * @returns {Function} Express middleware function - */ -function authenticateToken(userType = 'player') { - return async (req, res, next) => { - const authHeader = req.headers.authorization; - const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN - - if (!token) { - return res.status(401).json({ - error: 'Access token required', - code: 'TOKEN_MISSING', - }); - } - - try { - // Verify token - const decoded = jwt.verify(token, process.env.JWT_SECRET); - - // Check token type matches required type - if (decoded.type !== userType) { - return res.status(403).json({ - error: 'Invalid token type', - code: 'INVALID_TOKEN_TYPE', - }); - } - - // Get user from database - const tableName = userType === 'admin' ? 'admin_users' : 'players'; - const user = await db(tableName) - .where('id', decoded.userId) - .first(); - - if (!user) { - return res.status(403).json({ - error: 'User not found', - code: 'USER_NOT_FOUND', - }); - } - - // Check if user is active - if (userType === 'player' && user.account_status !== 'active') { - return res.status(403).json({ - error: 'Account is not active', - code: 'ACCOUNT_INACTIVE', - status: user.account_status, - }); - } - - if (userType === 'admin' && !user.is_active) { - return res.status(403).json({ - error: 'Admin account is not active', - code: 'ADMIN_INACTIVE', - }); - } - - // Attach user to request - req.user = user; - req.token = decoded; - - // Update last active timestamp for players - if (userType === 'player') { - // Don't await this to avoid slowing down requests - db('players') - .where('id', user.id) - .update({ last_active_at: new Date() }) - .catch(error => { - logger.error('Failed to update last_active_at:', error); - }); - } - - next(); - } catch (error) { - if (error.name === 'TokenExpiredError') { - return res.status(401).json({ - error: 'Token expired', - code: 'TOKEN_EXPIRED', - }); - } else if (error.name === 'JsonWebTokenError') { - return res.status(403).json({ - error: 'Invalid token', - code: 'INVALID_TOKEN', - }); - } else { - logger.error('Authentication error:', error); - return res.status(500).json({ - error: 'Authentication failed', - code: 'AUTH_ERROR', - }); - } - } - }; -} - -/** - * Optional authentication - sets user if token is provided but doesn't require it - * @param {string} userType - 'player' or 'admin' - * @returns {Function} Express middleware function - */ -function optionalAuth(userType = 'player') { - return async (req, res, next) => { - const authHeader = req.headers.authorization; - const token = authHeader && authHeader.split(' ')[1]; - - if (!token) { - return next(); - } - - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); - - if (decoded.type === userType) { - const tableName = userType === 'admin' ? 'admin_users' : 'players'; - const user = await db(tableName) - .where('id', decoded.userId) - .first(); - - if (user && ((userType === 'player' && user.account_status === 'active') || - (userType === 'admin' && user.is_active))) { - req.user = user; - req.token = decoded; - } - } - } catch (error) { - // Ignore errors in optional auth - logger.debug('Optional auth failed:', error.message); - } - - next(); - }; -} - -/** - * Check if user has specific permission (for admin users) - * @param {string} permission - Required permission - * @returns {Function} Express middleware function - */ -function requirePermission(permission) { - return (req, res, next) => { - if (!req.user) { - return res.status(401).json({ - error: 'Authentication required', - code: 'AUTH_REQUIRED', - }); - } - - // Super admins have all permissions - if (req.user.role === 'super_admin') { - return next(); - } - - // Check specific permission - const permissions = req.user.permissions || {}; - if (!permissions[permission]) { - return res.status(403).json({ - error: 'Insufficient permissions', - code: 'INSUFFICIENT_PERMISSIONS', - required: permission, - }); - } - - next(); - }; -} - -/** - * Check if user has specific role - * @param {string|string[]} roles - Required role(s) - * @returns {Function} Express middleware function - */ -function requireRole(roles) { - const requiredRoles = Array.isArray(roles) ? roles : [roles]; - - return (req, res, next) => { - if (!req.user) { - return res.status(401).json({ - error: 'Authentication required', - code: 'AUTH_REQUIRED', - }); - } - - if (!requiredRoles.includes(req.user.role)) { - return res.status(403).json({ - error: 'Insufficient role', - code: 'INSUFFICIENT_ROLE', - required: requiredRoles, - current: req.user.role, - }); - } - - next(); - }; -} - -module.exports = { - authenticateToken, - optionalAuth, - requirePermission, - requireRole, -}; diff --git a/src/middleware/auth.middleware.js b/src/middleware/auth.middleware.js index 571ba0f..f5d9f5b 100644 --- a/src/middleware/auth.middleware.js +++ b/src/middleware/auth.middleware.js @@ -13,79 +13,79 @@ const logger = require('../utils/logger'); * @param {Function} next - Express next function */ async function authenticatePlayer(req, res, next) { - try { - const correlationId = req.correlationId; + try { + const correlationId = req.correlationId; + + // Extract token from Authorization header + const authHeader = req.get('Authorization'); + const token = extractTokenFromHeader(authHeader); - // Extract token from Authorization header - const authHeader = req.get('Authorization'); - const token = extractTokenFromHeader(authHeader); + if (!token) { + logger.warn('Player authentication failed - no token provided', { + correlationId, + ip: req.ip, + userAgent: req.get('User-Agent'), + path: req.path + }); - if (!token) { - logger.warn('Player authentication failed - no token provided', { - correlationId, - ip: req.ip, - userAgent: req.get('User-Agent'), - path: req.path, - }); + return res.status(401).json({ + error: 'Authentication required', + message: 'No authentication token provided', + correlationId + }); + } - return res.status(401).json({ - error: 'Authentication required', - message: 'No authentication token provided', - correlationId, - }); + // Verify the token + const decoded = verifyPlayerToken(token); + + // Add player information to request object + req.user = { + playerId: decoded.playerId, + email: decoded.email, + username: decoded.username, + type: 'player', + iat: decoded.iat, + exp: decoded.exp + }; + + logger.info('Player authenticated successfully', { + correlationId, + playerId: decoded.playerId, + username: decoded.username, + path: req.path, + method: req.method + }); + + next(); + + } catch (error) { + const correlationId = req.correlationId; + + logger.warn('Player authentication failed', { + correlationId, + error: error.message, + ip: req.ip, + userAgent: req.get('User-Agent'), + path: req.path + }); + + let statusCode = 401; + let message = 'Invalid authentication token'; + + if (error.message === 'Token expired') { + statusCode = 401; + message = 'Authentication token has expired'; + } else if (error.message === 'Invalid token') { + statusCode = 401; + message = 'Invalid authentication token'; + } + + return res.status(statusCode).json({ + error: 'Authentication failed', + message, + correlationId + }); } - - // Verify the token - const decoded = verifyPlayerToken(token); - - // Add player information to request object - req.user = { - playerId: decoded.playerId, - email: decoded.email, - username: decoded.username, - type: 'player', - iat: decoded.iat, - exp: decoded.exp, - }; - - logger.info('Player authenticated successfully', { - correlationId, - playerId: decoded.playerId, - username: decoded.username, - path: req.path, - method: req.method, - }); - - next(); - - } catch (error) { - const correlationId = req.correlationId; - - logger.warn('Player authentication failed', { - correlationId, - error: error.message, - ip: req.ip, - userAgent: req.get('User-Agent'), - path: req.path, - }); - - let statusCode = 401; - let message = 'Invalid authentication token'; - - if (error.message === 'Token expired') { - statusCode = 401; - message = 'Authentication token has expired'; - } else if (error.message === 'Invalid token') { - statusCode = 401; - message = 'Invalid authentication token'; - } - - return res.status(statusCode).json({ - error: 'Authentication failed', - message, - correlationId, - }); - } } /** @@ -96,47 +96,47 @@ async function authenticatePlayer(req, res, next) { * @param {Function} next - Express next function */ async function optionalPlayerAuth(req, res, next) { - try { - const authHeader = req.get('Authorization'); - const token = extractTokenFromHeader(authHeader); + try { + const authHeader = req.get('Authorization'); + const token = extractTokenFromHeader(authHeader); - if (token) { - try { - const decoded = verifyPlayerToken(token); - req.user = { - playerId: decoded.playerId, - email: decoded.email, - username: decoded.username, - type: 'player', - iat: decoded.iat, - exp: decoded.exp, - }; + if (token) { + try { + const decoded = verifyPlayerToken(token); + req.user = { + playerId: decoded.playerId, + email: decoded.email, + username: decoded.username, + type: 'player', + iat: decoded.iat, + exp: decoded.exp + }; - logger.info('Optional player authentication successful', { - correlationId: req.correlationId, - playerId: decoded.playerId, - username: decoded.username, + logger.info('Optional player authentication successful', { + correlationId: req.correlationId, + playerId: decoded.playerId, + username: decoded.username + }); + } catch (error) { + logger.warn('Optional player authentication failed', { + correlationId: req.correlationId, + error: error.message + }); + // Continue without authentication + } + } + + next(); + + } catch (error) { + // If there's an unexpected error, log it but continue + logger.error('Optional player authentication error', { + correlationId: req.correlationId, + error: error.message, + stack: error.stack }); - } catch (error) { - logger.warn('Optional player authentication failed', { - correlationId: req.correlationId, - error: error.message, - }); - // Continue without authentication - } + next(); } - - next(); - - } catch (error) { - // If there's an unexpected error, log it but continue - logger.error('Optional player authentication error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); - next(); - } } /** @@ -145,79 +145,79 @@ async function optionalPlayerAuth(req, res, next) { * @returns {Function} Express middleware function */ function requireOwnership(paramName = 'playerId') { - return (req, res, next) => { - try { - const correlationId = req.correlationId; - const authenticatedPlayerId = req.user?.playerId; - const resourcePlayerId = parseInt(req.params[paramName]); + return (req, res, next) => { + try { + const correlationId = req.correlationId; + const authenticatedPlayerId = req.user?.playerId; + const resourcePlayerId = parseInt(req.params[paramName]); - if (!authenticatedPlayerId) { - logger.warn('Ownership check failed - no authenticated user', { - correlationId, - path: req.path, - }); + if (!authenticatedPlayerId) { + logger.warn('Ownership check failed - no authenticated user', { + correlationId, + path: req.path + }); - return res.status(401).json({ - error: 'Authentication required', - message: 'You must be authenticated to access this resource', - correlationId, - }); - } + return res.status(401).json({ + error: 'Authentication required', + message: 'You must be authenticated to access this resource', + correlationId + }); + } - if (!resourcePlayerId || isNaN(resourcePlayerId)) { - logger.warn('Ownership check failed - invalid resource ID', { - correlationId, - paramName, - resourcePlayerId: req.params[paramName], - playerId: authenticatedPlayerId, - }); + if (!resourcePlayerId || isNaN(resourcePlayerId)) { + logger.warn('Ownership check failed - invalid resource ID', { + correlationId, + paramName, + resourcePlayerId: req.params[paramName], + playerId: authenticatedPlayerId + }); - return res.status(400).json({ - error: 'Invalid request', - message: 'Invalid resource identifier', - correlationId, - }); - } + return res.status(400).json({ + error: 'Invalid request', + message: 'Invalid resource identifier', + correlationId + }); + } - if (authenticatedPlayerId !== resourcePlayerId) { - logger.warn('Ownership check failed - access denied', { - correlationId, - authenticatedPlayerId, - resourcePlayerId, - username: req.user.username, - path: req.path, - }); + if (authenticatedPlayerId !== resourcePlayerId) { + logger.warn('Ownership check failed - access denied', { + correlationId, + authenticatedPlayerId, + resourcePlayerId, + username: req.user.username, + path: req.path + }); - return res.status(403).json({ - error: 'Access denied', - message: 'You can only access your own resources', - correlationId, - }); - } + return res.status(403).json({ + error: 'Access denied', + message: 'You can only access your own resources', + correlationId + }); + } - logger.info('Ownership check passed', { - correlationId, - playerId: authenticatedPlayerId, - username: req.user.username, - path: req.path, - }); + logger.info('Ownership check passed', { + correlationId, + playerId: authenticatedPlayerId, + username: req.user.username, + path: req.path + }); - next(); + next(); - } catch (error) { - logger.error('Ownership check error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); + } catch (error) { + logger.error('Ownership check error', { + correlationId: req.correlationId, + error: error.message, + stack: error.stack + }); - return res.status(500).json({ - error: 'Internal server error', - message: 'Failed to verify resource ownership', - correlationId: req.correlationId, - }); - } - }; + return res.status(500).json({ + error: 'Internal server error', + message: 'Failed to verify resource ownership', + correlationId: req.correlationId + }); + } + }; } /** @@ -228,33 +228,33 @@ function requireOwnership(paramName = 'playerId') { * @param {Function} next - Express next function */ function injectPlayerId(req, res, next) { - try { - if (req.user && req.user.playerId) { - req.params.playerId = req.user.playerId.toString(); + try { + if (req.user && req.user.playerId) { + req.params.playerId = req.user.playerId.toString(); + + logger.debug('Player ID injected into params', { + correlationId: req.correlationId, + playerId: req.user.playerId, + path: req.path + }); + } - logger.debug('Player ID injected into params', { - correlationId: req.correlationId, - playerId: req.user.playerId, - path: req.path, - }); + next(); + + } catch (error) { + logger.error('Player ID injection error', { + correlationId: req.correlationId, + error: error.message, + stack: error.stack + }); + + next(); // Continue even if injection fails } - - next(); - - } catch (error) { - logger.error('Player ID injection error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); - - next(); // Continue even if injection fails - } } module.exports = { - authenticatePlayer, - optionalPlayerAuth, - requireOwnership, - injectPlayerId, -}; + authenticatePlayer, + optionalPlayerAuth, + requireOwnership, + injectPlayerId +}; \ No newline at end of file diff --git a/src/middleware/combat.middleware.js b/src/middleware/combat.middleware.js deleted file mode 100644 index f264c2e..0000000 --- a/src/middleware/combat.middleware.js +++ /dev/null @@ -1,581 +0,0 @@ -/** - * Combat Middleware - * Provides combat-specific middleware functions for authentication, authorization, and validation - */ - -const db = require('../database/connection'); -const logger = require('../utils/logger'); -const { ValidationError, ConflictError, NotFoundError, ForbiddenError } = require('./error.middleware'); -const combatValidators = require('../validators/combat.validators'); - -/** - * Validate combat initiation request - */ -const validateCombatInitiation = (req, res, next) => { - try { - const { error, value } = combatValidators.validateInitiateCombat(req.body); - - if (error) { - const details = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - })); - - logger.warn('Combat initiation validation failed', { - correlationId: req.correlationId, - playerId: req.user?.id, - errors: details, - }); - - return res.status(400).json({ - error: 'Validation failed', - code: 'COMBAT_VALIDATION_ERROR', - details, - }); - } - - req.body = value; - next(); - } catch (error) { - logger.error('Combat validation middleware error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); - next(error); - } -}; - -/** - * Validate fleet position update request - */ -const validateFleetPositionUpdate = (req, res, next) => { - try { - const { error, value } = combatValidators.validateUpdateFleetPosition(req.body); - - if (error) { - const details = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - })); - - logger.warn('Fleet position validation failed', { - correlationId: req.correlationId, - playerId: req.user?.id, - fleetId: req.params.fleetId, - errors: details, - }); - - return res.status(400).json({ - error: 'Validation failed', - code: 'POSITION_VALIDATION_ERROR', - details, - }); - } - - req.body = value; - next(); - } catch (error) { - logger.error('Fleet position validation middleware error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); - next(error); - } -}; - -/** - * Validate combat history query parameters - */ -const validateCombatHistoryQuery = (req, res, next) => { - try { - const { error, value } = combatValidators.validateCombatHistoryQuery(req.query); - - if (error) { - const details = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - })); - - logger.warn('Combat history query validation failed', { - correlationId: req.correlationId, - playerId: req.user?.id, - errors: details, - }); - - return res.status(400).json({ - error: 'Invalid query parameters', - code: 'QUERY_VALIDATION_ERROR', - details, - }); - } - - req.query = value; - next(); - } catch (error) { - logger.error('Combat history query validation middleware error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); - next(error); - } -}; - -/** - * Validate combat queue query parameters (admin only) - */ -const validateCombatQueueQuery = (req, res, next) => { - try { - const { error, value } = combatValidators.validateCombatQueueQuery(req.query); - - if (error) { - const details = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - })); - - logger.warn('Combat queue query validation failed', { - correlationId: req.correlationId, - adminUser: req.user?.id, - errors: details, - }); - - return res.status(400).json({ - error: 'Invalid query parameters', - code: 'QUERY_VALIDATION_ERROR', - details, - }); - } - - req.query = value; - next(); - } catch (error) { - logger.error('Combat queue query validation middleware error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - }); - next(error); - } -}; - -/** - * Validate parameter IDs (battleId, fleetId, encounterId) - */ -const validateParams = (paramType) => { - return (req, res, next) => { - try { - let validator; - switch (paramType) { - case 'battleId': - validator = combatValidators.validateBattleIdParam; - break; - case 'fleetId': - validator = combatValidators.validateFleetIdParam; - break; - case 'encounterId': - validator = combatValidators.validateEncounterIdParam; - break; - default: - return res.status(500).json({ - error: 'Invalid parameter validation type', - code: 'INTERNAL_ERROR', - }); - } - - const { error, value } = validator(req.params); - - if (error) { - const details = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - })); - - logger.warn('Parameter validation failed', { - correlationId: req.correlationId, - paramType, - params: req.params, - errors: details, - }); - - return res.status(400).json({ - error: 'Invalid parameter', - code: 'PARAM_VALIDATION_ERROR', - details, - }); - } - - req.params = { ...req.params, ...value }; - next(); - } catch (error) { - logger.error('Parameter validation middleware error', { - correlationId: req.correlationId, - paramType, - error: error.message, - stack: error.stack, - }); - next(error); - } - }; -}; - -/** - * Check if player owns the specified fleet - */ -const checkFleetOwnership = async (req, res, next) => { - try { - const playerId = req.user.id; - const fleetId = parseInt(req.params.fleetId); - - logger.debug('Checking fleet ownership', { - correlationId: req.correlationId, - playerId, - fleetId, - }); - - const fleet = await db('fleets') - .where('id', fleetId) - .where('player_id', playerId) - .first(); - - if (!fleet) { - logger.warn('Fleet ownership check failed', { - correlationId: req.correlationId, - playerId, - fleetId, - }); - - return res.status(404).json({ - error: 'Fleet not found or access denied', - code: 'FLEET_NOT_FOUND', - }); - } - - req.fleet = fleet; - next(); - } catch (error) { - logger.error('Fleet ownership check middleware error', { - correlationId: req.correlationId, - playerId: req.user?.id, - fleetId: req.params?.fleetId, - error: error.message, - stack: error.stack, - }); - next(error); - } -}; - -/** - * Check if player has access to the specified battle - */ -const checkBattleAccess = async (req, res, next) => { - try { - const playerId = req.user.id; - const battleId = parseInt(req.params.battleId); - - logger.debug('Checking battle access', { - correlationId: req.correlationId, - playerId, - battleId, - }); - - const battle = await db('battles') - .where('id', battleId) - .first(); - - if (!battle) { - logger.warn('Battle not found', { - correlationId: req.correlationId, - playerId, - battleId, - }); - - return res.status(404).json({ - error: 'Battle not found', - code: 'BATTLE_NOT_FOUND', - }); - } - - // Check if player is a participant - const participants = JSON.parse(battle.participants); - let hasAccess = false; - - // Check if player is the attacker - if (participants.attacker_player_id === playerId) { - hasAccess = true; - } - - // Check if player owns the defending fleet - if (participants.defender_fleet_id) { - const defenderFleet = await db('fleets') - .where('id', participants.defender_fleet_id) - .where('player_id', playerId) - .first(); - if (defenderFleet) hasAccess = true; - } - - // Check if player owns the defending colony - if (participants.defender_colony_id) { - const defenderColony = await db('colonies') - .where('id', participants.defender_colony_id) - .where('player_id', playerId) - .first(); - if (defenderColony) hasAccess = true; - } - - if (!hasAccess) { - logger.warn('Battle access denied', { - correlationId: req.correlationId, - playerId, - battleId, - }); - - return res.status(403).json({ - error: 'Access denied to this battle', - code: 'BATTLE_ACCESS_DENIED', - }); - } - - req.battle = battle; - next(); - } catch (error) { - logger.error('Battle access check middleware error', { - correlationId: req.correlationId, - playerId: req.user?.id, - battleId: req.params?.battleId, - error: error.message, - stack: error.stack, - }); - next(error); - } -}; - -/** - * Check combat cooldown to prevent spam attacks - */ -const checkCombatCooldown = async (req, res, next) => { - try { - const playerId = req.user.id; - const cooldownMinutes = parseInt(process.env.COMBAT_COOLDOWN_MINUTES) || 5; - - logger.debug('Checking combat cooldown', { - correlationId: req.correlationId, - playerId, - cooldownMinutes, - }); - - // Check if player has initiated combat recently - const recentCombat = await db('battles') - .join('combat_encounters', 'battles.id', 'combat_encounters.battle_id') - .leftJoin('fleets', 'combat_encounters.attacker_fleet_id', 'fleets.id') - .where('fleets.player_id', playerId) - .where('battles.started_at', '>', new Date(Date.now() - cooldownMinutes * 60 * 1000)) - .orderBy('battles.started_at', 'desc') - .first(); - - if (recentCombat) { - const timeRemaining = Math.ceil((new Date(recentCombat.started_at).getTime() + cooldownMinutes * 60 * 1000 - Date.now()) / 1000); - - logger.warn('Combat cooldown active', { - correlationId: req.correlationId, - playerId, - timeRemaining, - }); - - return res.status(429).json({ - error: 'Combat cooldown active', - code: 'COMBAT_COOLDOWN', - timeRemaining, - cooldownMinutes, - }); - } - - next(); - } catch (error) { - logger.error('Combat cooldown check middleware error', { - correlationId: req.correlationId, - playerId: req.user?.id, - error: error.message, - stack: error.stack, - }); - next(error); - } -}; - -/** - * Check if fleet is available for combat - */ -const checkFleetAvailability = async (req, res, next) => { - try { - const fleetId = req.body.attacker_fleet_id; - const playerId = req.user.id; - - logger.debug('Checking fleet availability', { - correlationId: req.correlationId, - playerId, - fleetId, - }); - - const fleet = await db('fleets') - .where('id', fleetId) - .where('player_id', playerId) - .first(); - - if (!fleet) { - return res.status(404).json({ - error: 'Fleet not found', - code: 'FLEET_NOT_FOUND', - }); - } - - // Check fleet status - if (fleet.fleet_status !== 'idle') { - logger.warn('Fleet not available for combat', { - correlationId: req.correlationId, - playerId, - fleetId, - currentStatus: fleet.fleet_status, - }); - - return res.status(409).json({ - error: `Fleet is currently ${fleet.fleet_status} and cannot engage in combat`, - code: 'FLEET_UNAVAILABLE', - currentStatus: fleet.fleet_status, - }); - } - - // Check if fleet has ships - const shipCount = await db('fleet_ships') - .where('fleet_id', fleetId) - .sum('quantity as total') - .first(); - - if (!shipCount.total || shipCount.total === 0) { - logger.warn('Fleet has no ships', { - correlationId: req.correlationId, - playerId, - fleetId, - }); - - return res.status(400).json({ - error: 'Fleet has no ships available for combat', - code: 'FLEET_EMPTY', - }); - } - - req.attackerFleet = fleet; - next(); - } catch (error) { - logger.error('Fleet availability check middleware error', { - correlationId: req.correlationId, - playerId: req.user?.id, - fleetId: req.body?.attacker_fleet_id, - error: error.message, - stack: error.stack, - }); - next(error); - } -}; - -/** - * Rate limiting for combat operations - */ -const combatRateLimit = (maxRequests = 10, windowMinutes = 15) => { - const requests = new Map(); - - return (req, res, next) => { - try { - const playerId = req.user.id; - const now = Date.now(); - const windowMs = windowMinutes * 60 * 1000; - - if (!requests.has(playerId)) { - requests.set(playerId, []); - } - - const playerRequests = requests.get(playerId); - - // Remove old requests outside the window - const validRequests = playerRequests.filter(timestamp => now - timestamp < windowMs); - requests.set(playerId, validRequests); - - // Check if limit exceeded - if (validRequests.length >= maxRequests) { - logger.warn('Combat rate limit exceeded', { - correlationId: req.correlationId, - playerId, - requestCount: validRequests.length, - maxRequests, - windowMinutes, - }); - - return res.status(429).json({ - error: 'Rate limit exceeded', - code: 'COMBAT_RATE_LIMIT', - maxRequests, - windowMinutes, - retryAfter: Math.ceil((validRequests[0] + windowMs - now) / 1000), - }); - } - - // Add current request - validRequests.push(now); - requests.set(playerId, validRequests); - - next(); - } catch (error) { - logger.error('Combat rate limit middleware error', { - correlationId: req.correlationId, - playerId: req.user?.id, - error: error.message, - stack: error.stack, - }); - next(error); - } - }; -}; - -/** - * Log combat actions for audit trail - */ -const logCombatAction = (action) => { - return (req, res, next) => { - try { - logger.info('Combat action attempted', { - correlationId: req.correlationId, - playerId: req.user?.id, - action, - params: req.params, - body: req.body, - query: req.query, - timestamp: new Date().toISOString(), - }); - - next(); - } catch (error) { - logger.error('Combat action logging middleware error', { - correlationId: req.correlationId, - action, - error: error.message, - stack: error.stack, - }); - next(error); - } - }; -}; - -module.exports = { - validateCombatInitiation, - validateFleetPositionUpdate, - validateCombatHistoryQuery, - validateCombatQueueQuery, - validateParams, - checkFleetOwnership, - checkBattleAccess, - checkCombatCooldown, - checkFleetAvailability, - combatRateLimit, - logCombatAction, -}; diff --git a/src/middleware/cors.js b/src/middleware/cors.js index 677b031..c30581b 100644 --- a/src/middleware/cors.js +++ b/src/middleware/cors.js @@ -6,18 +6,18 @@ const cors = require('cors'); // Configure CORS options const corsOptions = { - origin(origin, callback) { + origin: function (origin, callback) { // Allow requests with no origin (mobile apps, postman, etc.) if (!origin) return callback(null, true); - + // In development, allow any origin if (process.env.NODE_ENV === 'development') { return callback(null, true); } - + // In production, check against allowed origins const allowedOrigins = (process.env.CORS_ORIGIN || 'http://localhost:3000').split(','); - + if (allowedOrigins.includes(origin)) { callback(null, true); } else { @@ -43,4 +43,4 @@ const corsOptions = { maxAge: 86400, // 24 hours }; -module.exports = cors(corsOptions); +module.exports = cors(corsOptions); \ No newline at end of file diff --git a/src/middleware/cors.middleware.js b/src/middleware/cors.middleware.js index 9a4944e..5623042 100644 --- a/src/middleware/cors.middleware.js +++ b/src/middleware/cors.middleware.js @@ -8,75 +8,67 @@ const logger = require('../utils/logger'); // CORS Configuration const CORS_CONFIG = { - development: { - origin: [ - 'http://localhost:3000', - 'http://localhost:3001', - 'http://127.0.0.1:3000', - 'http://127.0.0.1:3001', - 'http://0.0.0.0:3000', - 'http://0.0.0.0:3001', - 'http://localhost:5173', - 'http://127.0.0.1:5173', - 'http://0.0.0.0:5173', - 'http://localhost:4173', - 'http://127.0.0.1:4173', - 'http://0.0.0.0:4173', - ], - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: [ - 'Origin', - 'X-Requested-With', - 'Content-Type', - 'Accept', - 'Authorization', - 'X-Correlation-ID', - ], - exposedHeaders: ['X-Correlation-ID', 'X-Total-Count'], - maxAge: 86400, // 24 hours - }, - production: { - origin(origin, callback) { - // Allow requests with no origin (mobile apps, etc.) - if (!origin) return callback(null, true); - - const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || '').split(',').map(o => o.trim()); - - if (allowedOrigins.includes(origin)) { - return callback(null, true); - } - - logger.warn('CORS origin blocked', { origin }); - callback(new Error('Not allowed by CORS')); + development: { + origin: [ + 'http://localhost:3000', + 'http://localhost:3001', + 'http://127.0.0.1:3000', + 'http://127.0.0.1:3001' + ], + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], + allowedHeaders: [ + 'Origin', + 'X-Requested-With', + 'Content-Type', + 'Accept', + 'Authorization', + 'X-Correlation-ID' + ], + exposedHeaders: ['X-Correlation-ID', 'X-Total-Count'], + maxAge: 86400 // 24 hours }, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], - allowedHeaders: [ - 'Origin', - 'X-Requested-With', - 'Content-Type', - 'Accept', - 'Authorization', - 'X-Correlation-ID', - ], - exposedHeaders: ['X-Correlation-ID', 'X-Total-Count'], - maxAge: 3600, // 1 hour - }, - test: { - origin: true, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: [ - 'Origin', - 'X-Requested-With', - 'Content-Type', - 'Accept', - 'Authorization', - 'X-Correlation-ID', - ], - exposedHeaders: ['X-Correlation-ID', 'X-Total-Count'], - }, + production: { + origin: function (origin, callback) { + // Allow requests with no origin (mobile apps, etc.) + if (!origin) return callback(null, true); + + const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || '').split(',').map(o => o.trim()); + + if (allowedOrigins.includes(origin)) { + return callback(null, true); + } + + logger.warn('CORS origin blocked', { origin }); + callback(new Error('Not allowed by CORS')); + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], + allowedHeaders: [ + 'Origin', + 'X-Requested-With', + 'Content-Type', + 'Accept', + 'Authorization', + 'X-Correlation-ID' + ], + exposeddHeaders: ['X-Correlation-ID', 'X-Total-Count'], + maxAge: 3600 // 1 hour + }, + test: { + origin: true, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], + allowedHeaders: [ + 'Origin', + 'X-Requested-With', + 'Content-Type', + 'Accept', + 'Authorization', + 'X-Correlation-ID' + ], + exposedHeaders: ['X-Correlation-ID', 'X-Total-Count'] + } }; /** @@ -84,24 +76,24 @@ const CORS_CONFIG = { * @returns {Object} CORS configuration object */ function getCorsConfig() { - const env = process.env.NODE_ENV || 'development'; - const config = CORS_CONFIG[env] || CORS_CONFIG.development; + const env = process.env.NODE_ENV || 'development'; + const config = CORS_CONFIG[env] || CORS_CONFIG.development; - // Override with environment variables if provided - if (process.env.CORS_ALLOWED_ORIGINS) { - const origins = process.env.CORS_ALLOWED_ORIGINS.split(',').map(o => o.trim()); - config.origin = origins; - } + // Override with environment variables if provided + if (process.env.CORS_ALLOWED_ORIGINS) { + const origins = process.env.CORS_ALLOWED_ORIGINS.split(',').map(o => o.trim()); + config.origin = origins; + } - if (process.env.CORS_CREDENTIALS) { - config.credentials = process.env.CORS_CREDENTIALS === 'true'; - } + if (process.env.CORS_CREDENTIALS) { + config.credentials = process.env.CORS_CREDENTIALS === 'true'; + } - if (process.env.CORS_MAX_AGE) { - config.maxAge = parseInt(process.env.CORS_MAX_AGE); - } + if (process.env.CORS_MAX_AGE) { + config.maxAge = parseInt(process.env.CORS_MAX_AGE); + } - return config; + return config; } /** @@ -109,86 +101,86 @@ function getCorsConfig() { * @returns {Function} CORS middleware function */ function createCorsMiddleware() { - const config = getCorsConfig(); + const config = getCorsConfig(); + + logger.info('CORS middleware configured', { + environment: process.env.NODE_ENV || 'development', + origins: typeof config.origin === 'function' ? 'dynamic' : config.origin, + credentials: config.credentials, + methods: config.methods + }); - logger.info('CORS middleware configured', { - environment: process.env.NODE_ENV || 'development', - origins: typeof config.origin === 'function' ? 'dynamic' : config.origin, - credentials: config.credentials, - methods: config.methods, - }); + return cors({ + ...config, + // Override origin handler to add logging + origin: function(origin, callback) { + const correlationId = require('uuid').v4(); + + // Handle dynamic origin function + if (typeof config.origin === 'function') { + return config.origin(origin, (err, allowed) => { + if (err) { + logger.warn('CORS origin rejected', { + correlationId, + origin, + error: err.message + }); + } else if (allowed) { + logger.debug('CORS origin allowed', { + correlationId, + origin + }); + } + callback(err, allowed); + }); + } - return cors({ - ...config, - // Override origin handler to add logging - origin(origin, callback) { - const correlationId = require('uuid').v4(); + // Handle static origin configuration + if (config.origin === true) { + logger.debug('CORS origin allowed (wildcard)', { + correlationId, + origin + }); + return callback(null, true); + } + + if (Array.isArray(config.origin)) { + const allowed = config.origin.includes(origin); + + if (allowed) { + logger.debug('CORS origin allowed', { + correlationId, + origin + }); + } else { + logger.warn('CORS origin rejected', { + correlationId, + origin, + allowedOrigins: config.origin + }); + } + + return callback(null, allowed); + } + + // Single origin string + if (config.origin === origin) { + logger.debug('CORS origin allowed', { + correlationId, + origin + }); + return callback(null, true); + } - // Handle dynamic origin function - if (typeof config.origin === 'function') { - return config.origin(origin, (err, allowed) => { - if (err) { logger.warn('CORS origin rejected', { - correlationId, - origin, - error: err.message, + correlationId, + origin, + allowedOrigin: config.origin }); - } else if (allowed) { - logger.debug('CORS origin allowed', { - correlationId, - origin, - }); - } - callback(err, allowed); - }); - } - - // Handle static origin configuration - if (config.origin === true) { - logger.debug('CORS origin allowed (wildcard)', { - correlationId, - origin, - }); - return callback(null, true); - } - - if (Array.isArray(config.origin)) { - const allowed = config.origin.includes(origin); - - if (allowed) { - logger.debug('CORS origin allowed', { - correlationId, - origin, - }); - } else { - logger.warn('CORS origin rejected', { - correlationId, - origin, - allowedOrigins: config.origin, - }); + + callback(new Error('Not allowed by CORS')); } - - return callback(null, allowed); - } - - // Single origin string - if (config.origin === origin) { - logger.debug('CORS origin allowed', { - correlationId, - origin, - }); - return callback(null, true); - } - - logger.warn('CORS origin rejected', { - correlationId, - origin, - allowedOrigin: config.origin, - }); - - callback(new Error('Not allowed by CORS')); - }, - }); + }); } /** @@ -198,30 +190,30 @@ function createCorsMiddleware() { * @param {Function} next - Express next function */ function addSecurityHeaders(req, res, next) { - // Add Vary header for proper caching - res.vary('Origin'); - - // Add security headers - res.set({ - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - }); - - // Log cross-origin requests - const origin = req.get('Origin'); - if (origin && origin !== `${req.protocol}://${req.get('Host')}`) { - logger.debug('Cross-origin request', { - correlationId: req.correlationId, - origin, - method: req.method, - path: req.path, - userAgent: req.get('User-Agent'), + // Add Vary header for proper caching + res.vary('Origin'); + + // Add security headers + res.set({ + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'X-XSS-Protection': '1; mode=block', + 'Referrer-Policy': 'strict-origin-when-cross-origin' }); - } - next(); + // Log cross-origin requests + const origin = req.get('Origin'); + if (origin && origin !== `${req.protocol}://${req.get('Host')}`) { + logger.debug('Cross-origin request', { + correlationId: req.correlationId, + origin, + method: req.method, + path: req.path, + userAgent: req.get('User-Agent') + }); + } + + next(); } /** @@ -231,16 +223,16 @@ function addSecurityHeaders(req, res, next) { * @param {Function} next - Express next function */ function handlePreflight(req, res, next) { - if (req.method === 'OPTIONS') { - logger.debug('CORS preflight request', { - correlationId: req.correlationId, - origin: req.get('Origin'), - requestedMethod: req.get('Access-Control-Request-Method'), - requestedHeaders: req.get('Access-Control-Request-Headers'), - }); - } - - next(); + if (req.method === 'OPTIONS') { + logger.debug('CORS preflight request', { + correlationId: req.correlationId, + origin: req.get('Origin'), + requestedMethod: req.get('Access-Control-Request-Method'), + requestedHeaders: req.get('Access-Control-Request-Headers') + }); + } + + next(); } /** @@ -251,27 +243,27 @@ function handlePreflight(req, res, next) { * @param {Function} next - Express next function */ function handleCorsError(err, req, res, next) { - if (err.message === 'Not allowed by CORS') { - logger.warn('CORS request blocked', { - correlationId: req.correlationId, - origin: req.get('Origin'), - method: req.method, - path: req.path, - ip: req.ip, - userAgent: req.get('User-Agent'), - }); + if (err.message === 'Not allowed by CORS') { + logger.warn('CORS request blocked', { + correlationId: req.correlationId, + origin: req.get('Origin'), + method: req.method, + path: req.path, + ip: req.ip, + userAgent: req.get('User-Agent') + }); - return res.status(403).json({ - error: 'CORS Policy Violation', - message: 'Cross-origin requests are not allowed from this origin', - correlationId: req.correlationId, - }); - } + return res.status(403).json({ + error: 'CORS Policy Violation', + message: 'Cross-origin requests are not allowed from this origin', + correlationId: req.correlationId + }); + } - next(err); + next(err); } // Create and export the configured CORS middleware const corsMiddleware = createCorsMiddleware(); -module.exports = corsMiddleware; +module.exports = corsMiddleware; \ No newline at end of file diff --git a/src/middleware/cors.middleware.js.backup b/src/middleware/cors.middleware.js.backup deleted file mode 100644 index 3e7264b..0000000 --- a/src/middleware/cors.middleware.js.backup +++ /dev/null @@ -1,269 +0,0 @@ -/** - * CORS Configuration Middleware - * Handles Cross-Origin Resource Sharing with environment-based configuration - */ - -const cors = require('cors'); -const logger = require('../utils/logger'); - -// CORS Configuration -const CORS_CONFIG = { - development: { - origin: [ - 'http://localhost:3000', - 'http://localhost:3001', - 'http://127.0.0.1:3000', - 'http://127.0.0.1:3001', - ], - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: [ - 'Origin', - 'X-Requested-With', - 'Content-Type', - 'Accept', - 'Authorization', - 'X-Correlation-ID', - ], - exposedHeaders: ['X-Correlation-ID', 'X-Total-Count'], - maxAge: 86400, // 24 hours - }, - production: { - origin(origin, callback) { - // Allow requests with no origin (mobile apps, etc.) - if (!origin) return callback(null, true); - - const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || '').split(',').map(o => o.trim()); - - if (allowedOrigins.includes(origin)) { - return callback(null, true); - } - - logger.warn('CORS origin blocked', { origin }); - callback(new Error('Not allowed by CORS')); - }, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], - allowedHeaders: [ - 'Origin', - 'X-Requested-With', - 'Content-Type', - 'Accept', - 'Authorization', - 'X-Correlation-ID', - ], - exposeddHeaders: ['X-Correlation-ID', 'X-Total-Count'], - maxAge: 3600, // 1 hour - }, - test: { - origin: true, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: [ - 'Origin', - 'X-Requested-With', - 'Content-Type', - 'Accept', - 'Authorization', - 'X-Correlation-ID', - ], - exposedHeaders: ['X-Correlation-ID', 'X-Total-Count'], - }, -}; - -/** - * Get CORS configuration for current environment - * @returns {Object} CORS configuration object - */ -function getCorsConfig() { - const env = process.env.NODE_ENV || 'development'; - const config = CORS_CONFIG[env] || CORS_CONFIG.development; - - // Override with environment variables if provided - if (process.env.CORS_ALLOWED_ORIGINS) { - const origins = process.env.CORS_ALLOWED_ORIGINS.split(',').map(o => o.trim()); - config.origin = origins; - } - - if (process.env.CORS_CREDENTIALS) { - config.credentials = process.env.CORS_CREDENTIALS === 'true'; - } - - if (process.env.CORS_MAX_AGE) { - config.maxAge = parseInt(process.env.CORS_MAX_AGE); - } - - return config; -} - -/** - * Create CORS middleware with logging - * @returns {Function} CORS middleware function - */ -function createCorsMiddleware() { - const config = getCorsConfig(); - - logger.info('CORS middleware configured', { - environment: process.env.NODE_ENV || 'development', - origins: typeof config.origin === 'function' ? 'dynamic' : config.origin, - credentials: config.credentials, - methods: config.methods, - }); - - return cors({ - ...config, - // Override origin handler to add logging - origin(origin, callback) { - const correlationId = require('uuid').v4(); - - // Handle dynamic origin function - if (typeof config.origin === 'function') { - return config.origin(origin, (err, allowed) => { - if (err) { - logger.warn('CORS origin rejected', { - correlationId, - origin, - error: err.message, - }); - } else if (allowed) { - logger.debug('CORS origin allowed', { - correlationId, - origin, - }); - } - callback(err, allowed); - }); - } - - // Handle static origin configuration - if (config.origin === true) { - logger.debug('CORS origin allowed (wildcard)', { - correlationId, - origin, - }); - return callback(null, true); - } - - if (Array.isArray(config.origin)) { - const allowed = config.origin.includes(origin); - - if (allowed) { - logger.debug('CORS origin allowed', { - correlationId, - origin, - }); - } else { - logger.warn('CORS origin rejected', { - correlationId, - origin, - allowedOrigins: config.origin, - }); - } - - return callback(null, allowed); - } - - // Single origin string - if (config.origin === origin) { - logger.debug('CORS origin allowed', { - correlationId, - origin, - }); - return callback(null, true); - } - - logger.warn('CORS origin rejected', { - correlationId, - origin, - allowedOrigin: config.origin, - }); - - callback(new Error('Not allowed by CORS')); - }, - }); -} - -/** - * Middleware to add security headers for CORS - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next function - */ -function addSecurityHeaders(req, res, next) { - // Add Vary header for proper caching - res.vary('Origin'); - - // Add security headers - res.set({ - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - }); - - // Log cross-origin requests - const origin = req.get('Origin'); - if (origin && origin !== `${req.protocol}://${req.get('Host')}`) { - logger.debug('Cross-origin request', { - correlationId: req.correlationId, - origin, - method: req.method, - path: req.path, - userAgent: req.get('User-Agent'), - }); - } - - next(); -} - -/** - * Handle preflight OPTIONS requests - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next function - */ -function handlePreflight(req, res, next) { - if (req.method === 'OPTIONS') { - logger.debug('CORS preflight request', { - correlationId: req.correlationId, - origin: req.get('Origin'), - requestedMethod: req.get('Access-Control-Request-Method'), - requestedHeaders: req.get('Access-Control-Request-Headers'), - }); - } - - next(); -} - -/** - * CORS error handler - * @param {Error} err - CORS error - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next function - */ -function handleCorsError(err, req, res, next) { - if (err.message === 'Not allowed by CORS') { - logger.warn('CORS request blocked', { - correlationId: req.correlationId, - origin: req.get('Origin'), - method: req.method, - path: req.path, - ip: req.ip, - userAgent: req.get('User-Agent'), - }); - - return res.status(403).json({ - error: 'CORS Policy Violation', - message: 'Cross-origin requests are not allowed from this origin', - correlationId: req.correlationId, - }); - } - - next(err); -} - -// Create and export the configured CORS middleware -const corsMiddleware = createCorsMiddleware(); - -module.exports = corsMiddleware; diff --git a/src/middleware/error-handler.js b/src/middleware/error-handler.js index 8815a53..b6dc5a5 100644 --- a/src/middleware/error-handler.js +++ b/src/middleware/error-handler.js @@ -76,7 +76,7 @@ function errorHandler(error, req, res, next) { // Default error response let statusCode = error.statusCode || 500; - const errorResponse = { + let errorResponse = { error: error.message || 'Internal server error', code: error.name || 'INTERNAL_ERROR', timestamp: new Date().toISOString(), @@ -89,132 +89,132 @@ function errorHandler(error, req, res, next) { // Handle specific error types switch (error.name) { - case 'ValidationError': - statusCode = 400; - errorResponse.details = error.details; - logger.warn('Validation error', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - error: error.message, - details: error.details, - }); - break; + case 'ValidationError': + statusCode = 400; + errorResponse.details = error.details; + logger.warn('Validation error', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + error: error.message, + details: error.details, + }); + break; - case 'ConflictError': - statusCode = 409; - errorResponse.details = error.details; - logger.warn('Conflict error', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - error: error.message, - details: error.details, - }); - break; + case 'ConflictError': + statusCode = 409; + errorResponse.details = error.details; + logger.warn('Conflict error', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + error: error.message, + details: error.details, + }); + break; - case 'NotFoundError': - statusCode = 404; - errorResponse.details = error.details; - logger.warn('Not found error', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - error: error.message, - }); - break; + case 'NotFoundError': + statusCode = 404; + errorResponse.details = error.details; + logger.warn('Not found error', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + error: error.message, + }); + break; - case 'ForbiddenError': - statusCode = 403; - errorResponse.details = error.details; - logger.warn('Forbidden error', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - error: error.message, - userId: req.user?.id, - }); - break; + case 'ForbiddenError': + statusCode = 403; + errorResponse.details = error.details; + logger.warn('Forbidden error', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + error: error.message, + userId: req.user?.id, + }); + break; - case 'RateLimitError': - statusCode = 429; - errorResponse.details = error.details; - logger.warn('Rate limit error', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - ip: req.ip, - error: error.message, - }); - break; + case 'RateLimitError': + statusCode = 429; + errorResponse.details = error.details; + logger.warn('Rate limit error', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + ip: req.ip, + error: error.message, + }); + break; - case 'JsonWebTokenError': - statusCode = 401; - errorResponse.error = 'Invalid authentication token'; - errorResponse.code = 'INVALID_TOKEN'; - logger.warn('JWT error', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - error: error.message, - }); - break; + case 'JsonWebTokenError': + statusCode = 401; + errorResponse.error = 'Invalid authentication token'; + errorResponse.code = 'INVALID_TOKEN'; + logger.warn('JWT error', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + error: error.message, + }); + break; - case 'TokenExpiredError': - statusCode = 401; - errorResponse.error = 'Authentication token expired'; - errorResponse.code = 'TOKEN_EXPIRED'; - logger.warn('JWT expired', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - error: error.message, - }); - break; + case 'TokenExpiredError': + statusCode = 401; + errorResponse.error = 'Authentication token expired'; + errorResponse.code = 'TOKEN_EXPIRED'; + logger.warn('JWT expired', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + error: error.message, + }); + break; - case 'CastError': - case 'ValidationError': - // Database validation errors - statusCode = 400; - errorResponse.error = 'Invalid data provided'; - errorResponse.code = 'INVALID_DATA'; - logger.warn('Database validation error', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - error: error.message, - }); - break; + case 'CastError': + case 'ValidationError': + // Database validation errors + statusCode = 400; + errorResponse.error = 'Invalid data provided'; + errorResponse.code = 'INVALID_DATA'; + logger.warn('Database validation error', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + error: error.message, + }); + break; - case 'ServiceError': - statusCode = 500; - logger.error('Service error', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - error: error.message, - originalError: error.originalError?.message, - stack: error.stack, - }); - break; + case 'ServiceError': + statusCode = 500; + logger.error('Service error', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + error: error.message, + originalError: error.originalError?.message, + stack: error.stack, + }); + break; - default: - // Log unexpected errors - logger.error('Unhandled error', { - correlationId: req.correlationId, - path: req.path, - method: req.method, - error: error.message, - stack: error.stack, - name: error.name, - }); + default: + // Log unexpected errors + logger.error('Unhandled error', { + correlationId: req.correlationId, + path: req.path, + method: req.method, + error: error.message, + stack: error.stack, + name: error.name, + }); - // Don't expose internal errors in production - if (process.env.NODE_ENV === 'production') { - errorResponse.error = 'Internal server error'; - errorResponse.code = 'INTERNAL_ERROR'; - } - break; + // Don't expose internal errors in production + if (process.env.NODE_ENV === 'production') { + errorResponse.error = 'Internal server error'; + errorResponse.code = 'INTERNAL_ERROR'; + } + break; } // Add stack trace in development @@ -280,4 +280,4 @@ module.exports = { ForbiddenError, ServiceError, RateLimitError, -}; +}; \ No newline at end of file diff --git a/src/middleware/error.middleware.js b/src/middleware/error.middleware.js index b80dc4c..3e6624e 100644 --- a/src/middleware/error.middleware.js +++ b/src/middleware/error.middleware.js @@ -9,70 +9,70 @@ const logger = require('../utils/logger'); * Custom error classes for better error handling */ class ValidationError extends Error { - constructor(message, details = null) { - super(message); - this.name = 'ValidationError'; - this.statusCode = 400; - this.details = details; - } + constructor(message, details = null) { + super(message); + this.name = 'ValidationError'; + this.statusCode = 400; + this.details = details; + } } class AuthenticationError extends Error { - constructor(message = 'Authentication failed') { - super(message); - this.name = 'AuthenticationError'; - this.statusCode = 401; - } + constructor(message = 'Authentication failed') { + super(message); + this.name = 'AuthenticationError'; + this.statusCode = 401; + } } class AuthorizationError extends Error { - constructor(message = 'Access denied') { - super(message); - this.name = 'AuthorizationError'; - this.statusCode = 403; - } + constructor(message = 'Access denied') { + super(message); + this.name = 'AuthorizationError'; + this.statusCode = 403; + } } class NotFoundError extends Error { - constructor(message = 'Resource not found') { - super(message); - this.name = 'NotFoundError'; - this.statusCode = 404; - } + constructor(message = 'Resource not found') { + super(message); + this.name = 'NotFoundError'; + this.statusCode = 404; + } } class ConflictError extends Error { - constructor(message = 'Resource conflict') { - super(message); - this.name = 'ConflictError'; - this.statusCode = 409; - } + constructor(message = 'Resource conflict') { + super(message); + this.name = 'ConflictError'; + this.statusCode = 409; + } } class RateLimitError extends Error { - constructor(message = 'Rate limit exceeded') { - super(message); - this.name = 'RateLimitError'; - this.statusCode = 429; - } + constructor(message = 'Rate limit exceeded') { + super(message); + this.name = 'RateLimitError'; + this.statusCode = 429; + } } class ServiceError extends Error { - constructor(message = 'Internal service error', originalError = null) { - super(message); - this.name = 'ServiceError'; - this.statusCode = 500; - this.originalError = originalError; - } + constructor(message = 'Internal service error', originalError = null) { + super(message); + this.name = 'ServiceError'; + this.statusCode = 500; + this.originalError = originalError; + } } class DatabaseError extends Error { - constructor(message = 'Database operation failed', originalError = null) { - super(message); - this.name = 'DatabaseError'; - this.statusCode = 500; - this.originalError = originalError; - } + constructor(message = 'Database operation failed', originalError = null) { + super(message); + this.name = 'DatabaseError'; + this.statusCode = 500; + this.originalError = originalError; + } } /** @@ -83,41 +83,41 @@ class DatabaseError extends Error { * @param {Function} next - Express next function */ function errorHandler(error, req, res, next) { - const correlationId = req.correlationId || 'unknown'; - const startTime = Date.now(); + const correlationId = req.correlationId || 'unknown'; + const startTime = Date.now(); - // Don't handle if response already sent - if (res.headersSent) { - logger.error('Error occurred after response sent', { - correlationId, - error: error.message, - stack: error.stack, + // Don't handle if response already sent + if (res.headersSent) { + logger.error('Error occurred after response sent', { + correlationId, + error: error.message, + stack: error.stack + }); + return next(error); + } + + // Log the error + logError(error, req, correlationId); + + // Determine error details + const errorResponse = createErrorResponse(error, req, correlationId); + + // Set appropriate headers + res.set({ + 'Content-Type': 'application/json', + 'X-Correlation-ID': correlationId }); - return next(error); - } - // Log the error - logError(error, req, correlationId); + // Send error response + res.status(errorResponse.statusCode).json(errorResponse.body); - // Determine error details - const errorResponse = createErrorResponse(error, req, correlationId); - - // Set appropriate headers - res.set({ - 'Content-Type': 'application/json', - 'X-Correlation-ID': correlationId, - }); - - // Send error response - res.status(errorResponse.statusCode).json(errorResponse.body); - - // Log response time for error handling - const duration = Date.now() - startTime; - logger.info('Error response sent', { - correlationId, - statusCode: errorResponse.statusCode, - duration: `${duration}ms`, - }); + // Log response time for error handling + const duration = Date.now() - startTime; + logger.info('Error response sent', { + correlationId, + statusCode: errorResponse.statusCode, + duration: `${duration}ms` + }); } /** @@ -127,62 +127,62 @@ function errorHandler(error, req, res, next) { * @param {string} correlationId - Request correlation ID */ function logError(error, req, correlationId) { - const errorInfo = { - correlationId, - name: error.name, - message: error.message, - statusCode: error.statusCode || 500, - method: req.method, - url: req.originalUrl, - path: req.path, - ip: req.ip, - userAgent: req.get('User-Agent'), - userId: req.user?.playerId || req.user?.adminId, - userType: req.user?.type, - timestamp: new Date().toISOString(), - }; + const errorInfo = { + correlationId, + name: error.name, + message: error.message, + statusCode: error.statusCode || 500, + method: req.method, + url: req.originalUrl, + path: req.path, + ip: req.ip, + userAgent: req.get('User-Agent'), + userId: req.user?.playerId || req.user?.adminId, + userType: req.user?.type, + timestamp: new Date().toISOString() + }; - // Add stack trace for server errors - if (!error.statusCode || error.statusCode >= 500) { - errorInfo.stack = error.stack; - - // Add original error if available - if (error.originalError) { - errorInfo.originalError = { - name: error.originalError.name, - message: error.originalError.message, - stack: error.originalError.stack, - }; + // Add stack trace for server errors + if (!error.statusCode || error.statusCode >= 500) { + errorInfo.stack = error.stack; + + // Add original error if available + if (error.originalError) { + errorInfo.originalError = { + name: error.originalError.name, + message: error.originalError.message, + stack: error.originalError.stack + }; + } } - } - // Add request body for debugging (sanitized) - if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) { - errorInfo.requestBody = sanitizeForLogging(req.body); - } + // Add request body for debugging (sanitized) + if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) { + errorInfo.requestBody = sanitizeForLogging(req.body); + } - // Add query parameters - if (Object.keys(req.query).length > 0) { - errorInfo.queryParams = req.query; - } + // Add query parameters + if (Object.keys(req.query).length > 0) { + errorInfo.queryParams = req.query; + } - // Determine log level - const statusCode = error.statusCode || 500; - if (statusCode >= 500) { - logger.error('Server error occurred', errorInfo); - } else if (statusCode >= 400) { - logger.warn('Client error occurred', errorInfo); - } else { - logger.info('Request completed with error', errorInfo); - } + // Determine log level + const statusCode = error.statusCode || 500; + if (statusCode >= 500) { + logger.error('Server error occurred', errorInfo); + } else if (statusCode >= 400) { + logger.warn('Client error occurred', errorInfo); + } else { + logger.info('Request completed with error', errorInfo); + } - // Audit sensitive errors - if (shouldAuditError(error, req)) { - logger.audit('Error occurred', { - ...errorInfo, - audit: true, - }); - } + // Audit sensitive errors + if (shouldAuditError(error, req)) { + logger.audit('Error occurred', { + ...errorInfo, + audit: true + }); + } } /** @@ -193,133 +193,133 @@ function logError(error, req, correlationId) { * @returns {Object} Error response object */ function createErrorResponse(error, req, correlationId) { - const statusCode = determineStatusCode(error); - const isDevelopment = process.env.NODE_ENV === 'development'; - const isProduction = process.env.NODE_ENV === 'production'; + const statusCode = determineStatusCode(error); + const isDevelopment = process.env.NODE_ENV === 'development'; + const isProduction = process.env.NODE_ENV === 'production'; - const baseResponse = { - error: true, - correlationId, - timestamp: new Date().toISOString(), - }; - - // Handle different error types - switch (error.name) { - case 'ValidationError': - return { - statusCode: 400, - body: { - ...baseResponse, - type: 'ValidationError', - message: 'Request validation failed', - details: error.details || error.message, - }, + const baseResponse = { + error: true, + correlationId, + timestamp: new Date().toISOString() }; - case 'AuthenticationError': - return { - statusCode: 401, - body: { - ...baseResponse, - type: 'AuthenticationError', - message: isProduction ? 'Authentication required' : error.message, - }, - }; + // Handle different error types + switch (error.name) { + case 'ValidationError': + return { + statusCode: 400, + body: { + ...baseResponse, + type: 'ValidationError', + message: 'Request validation failed', + details: error.details || error.message + } + }; - case 'AuthorizationError': - return { - statusCode: 403, - body: { - ...baseResponse, - type: 'AuthorizationError', - message: isProduction ? 'Access denied' : error.message, - }, - }; + case 'AuthenticationError': + return { + statusCode: 401, + body: { + ...baseResponse, + type: 'AuthenticationError', + message: isProduction ? 'Authentication required' : error.message + } + }; - case 'NotFoundError': - return { - statusCode: 404, - body: { - ...baseResponse, - type: 'NotFoundError', - message: error.message || 'Resource not found', - }, - }; + case 'AuthorizationError': + return { + statusCode: 403, + body: { + ...baseResponse, + type: 'AuthorizationError', + message: isProduction ? 'Access denied' : error.message + } + }; - case 'ConflictError': - return { - statusCode: 409, - body: { - ...baseResponse, - type: 'ConflictError', - message: error.message || 'Resource conflict', - }, - }; + case 'NotFoundError': + return { + statusCode: 404, + body: { + ...baseResponse, + type: 'NotFoundError', + message: error.message || 'Resource not found' + } + }; - case 'RateLimitError': - return { - statusCode: 429, - body: { - ...baseResponse, - type: 'RateLimitError', - message: error.message || 'Rate limit exceeded', - retryAfter: error.retryAfter, - }, - }; + case 'ConflictError': + return { + statusCode: 409, + body: { + ...baseResponse, + type: 'ConflictError', + message: error.message || 'Resource conflict' + } + }; - // Database errors - case 'DatabaseError': - case 'SequelizeError': - case 'QueryFailedError': - return { - statusCode: 500, - body: { - ...baseResponse, - type: 'DatabaseError', - message: isProduction ? 'Database operation failed' : error.message, - ...(isDevelopment && { stack: error.stack }), - }, - }; + case 'RateLimitError': + return { + statusCode: 429, + body: { + ...baseResponse, + type: 'RateLimitError', + message: error.message || 'Rate limit exceeded', + retryAfter: error.retryAfter + } + }; - // JWT errors - case 'JsonWebTokenError': - case 'TokenExpiredError': - case 'NotBeforeError': - return { - statusCode: 401, - body: { - ...baseResponse, - type: 'TokenError', - message: 'Invalid or expired token', - }, - }; + // Database errors + case 'DatabaseError': + case 'SequelizeError': + case 'QueryFailedError': + return { + statusCode: 500, + body: { + ...baseResponse, + type: 'DatabaseError', + message: isProduction ? 'Database operation failed' : error.message, + ...(isDevelopment && { stack: error.stack }) + } + }; - // Multer errors (file upload) - case 'MulterError': - return { - statusCode: 400, - body: { - ...baseResponse, - type: 'FileUploadError', - message: getMulterErrorMessage(error), - }, - }; + // JWT errors + case 'JsonWebTokenError': + case 'TokenExpiredError': + case 'NotBeforeError': + return { + statusCode: 401, + body: { + ...baseResponse, + type: 'TokenError', + message: 'Invalid or expired token' + } + }; - // Default server error - default: - return { - statusCode: statusCode >= 400 ? statusCode : 500, - body: { - ...baseResponse, - type: 'ServerError', - message: isProduction ? 'Internal server error' : error.message, - ...(isDevelopment && { - stack: error.stack, - originalError: error.originalError, - }), - }, - }; - } + // Multer errors (file upload) + case 'MulterError': + return { + statusCode: 400, + body: { + ...baseResponse, + type: 'FileUploadError', + message: getMulterErrorMessage(error) + } + }; + + // Default server error + default: + return { + statusCode: statusCode >= 400 ? statusCode : 500, + body: { + ...baseResponse, + type: 'ServerError', + message: isProduction ? 'Internal server error' : error.message, + ...(isDevelopment && { + stack: error.stack, + originalError: error.originalError + }) + } + }; + } } /** @@ -328,33 +328,33 @@ function createErrorResponse(error, req, correlationId) { * @returns {number} HTTP status code */ function determineStatusCode(error) { - // Use explicit status code if available - if (error.statusCode && typeof error.statusCode === 'number') { - return error.statusCode; - } + // Use explicit status code if available + if (error.statusCode && typeof error.statusCode === 'number') { + return error.statusCode; + } - // Use status property if available - if (error.status && typeof error.status === 'number') { - return error.status; - } + // Use status property if available + if (error.status && typeof error.status === 'number') { + return error.status; + } - // Default mappings by error name - const statusMappings = { - ValidationError: 400, - CastError: 400, - JsonWebTokenError: 401, - TokenExpiredError: 401, - UnauthorizedError: 401, - AuthenticationError: 401, - ForbiddenError: 403, - AuthorizationError: 403, - NotFoundError: 404, - ConflictError: 409, - MulterError: 400, - RateLimitError: 429, - }; + // Default mappings by error name + const statusMappings = { + 'ValidationError': 400, + 'CastError': 400, + 'JsonWebTokenError': 401, + 'TokenExpiredError': 401, + 'UnauthorizedError': 401, + 'AuthenticationError': 401, + 'ForbiddenError': 403, + 'AuthorizationError': 403, + 'NotFoundError': 404, + 'ConflictError': 409, + 'MulterError': 400, + 'RateLimitError': 429 + }; - return statusMappings[error.name] || 500; + return statusMappings[error.name] || 500; } /** @@ -363,22 +363,22 @@ function determineStatusCode(error) { * @returns {string} User-friendly error message */ function getMulterErrorMessage(error) { - switch (error.code) { - case 'LIMIT_FILE_SIZE': - return 'File size too large'; - case 'LIMIT_FILE_COUNT': - return 'Too many files uploaded'; - case 'LIMIT_FIELD_KEY': - return 'Field name too long'; - case 'LIMIT_FIELD_VALUE': - return 'Field value too long'; - case 'LIMIT_FIELD_COUNT': - return 'Too many fields'; - case 'LIMIT_UNEXPECTED_FILE': - return 'Unexpected file field'; - default: - return 'File upload error'; - } + switch (error.code) { + case 'LIMIT_FILE_SIZE': + return 'File size too large'; + case 'LIMIT_FILE_COUNT': + return 'Too many files uploaded'; + case 'LIMIT_FIELD_KEY': + return 'Field name too long'; + case 'LIMIT_FIELD_VALUE': + return 'Field value too long'; + case 'LIMIT_FIELD_COUNT': + return 'Too many fields'; + case 'LIMIT_UNEXPECTED_FILE': + return 'Unexpected file field'; + default: + return 'File upload error'; + } } /** @@ -387,30 +387,30 @@ function getMulterErrorMessage(error) { * @returns {Object} Sanitized data */ function sanitizeForLogging(data) { - if (!data || typeof data !== 'object') return data; + if (!data || typeof data !== 'object') return data; - try { - const sanitized = JSON.parse(JSON.stringify(data)); - const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash', 'authorization']; + try { + const sanitized = JSON.parse(JSON.stringify(data)); + const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash', 'authorization']; - function recursiveSanitize(obj) { - if (typeof obj !== 'object' || obj === null) return obj; + function recursiveSanitize(obj) { + if (typeof obj !== 'object' || obj === null) return obj; - Object.keys(obj).forEach(key => { - if (sensitiveFields.some(field => key.toLowerCase().includes(field))) { - obj[key] = '[REDACTED]'; - } else if (typeof obj[key] === 'object') { - recursiveSanitize(obj[key]); + Object.keys(obj).forEach(key => { + if (sensitiveFields.some(field => key.toLowerCase().includes(field))) { + obj[key] = '[REDACTED]'; + } else if (typeof obj[key] === 'object') { + recursiveSanitize(obj[key]); + } + }); + + return obj; } - }); - return obj; + return recursiveSanitize(sanitized); + } catch { + return '[SANITIZATION_ERROR]'; } - - return recursiveSanitize(sanitized); - } catch { - return '[SANITIZATION_ERROR]'; - } } /** @@ -420,25 +420,25 @@ function sanitizeForLogging(data) { * @returns {boolean} True if should audit */ function shouldAuditError(error, req) { - const statusCode = error.statusCode || 500; + const statusCode = error.statusCode || 500; - // Audit all server errors - if (statusCode >= 500) return true; + // Audit all server errors + if (statusCode >= 500) return true; - // Audit authentication/authorization errors - if (['AuthenticationError', 'AuthorizationError', 'JsonWebTokenError'].includes(error.name)) { - return true; - } + // Audit authentication/authorization errors + if (['AuthenticationError', 'AuthorizationError', 'JsonWebTokenError'].includes(error.name)) { + return true; + } - // Audit admin-related errors - if (req.user?.type === 'admin') return true; + // Audit admin-related errors + if (req.user?.type === 'admin') return true; - // Audit security-related endpoints - if (req.path.includes('/auth/') || req.path.includes('/admin/')) { - return true; - } + // Audit security-related endpoints + if (req.path.includes('/auth/') || req.path.includes('/admin/')) { + return true; + } - return false; + return false; } /** @@ -447,9 +447,9 @@ function shouldAuditError(error, req) { * @returns {Function} Wrapped route handler */ function asyncHandler(fn) { - return (req, res, next) => { - Promise.resolve(fn(req, res, next)).catch(next); - }; + return (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; } /** @@ -459,21 +459,21 @@ function asyncHandler(fn) { * @param {Function} next - Express next function */ function notFoundHandler(req, res, next) { - const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`); - next(error); + const error = new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`); + next(error); } module.exports = { - errorHandler, - notFoundHandler, - asyncHandler, - // Export error classes - ValidationError, - AuthenticationError, - AuthorizationError, - NotFoundError, - ConflictError, - RateLimitError, - ServiceError, - DatabaseError, -}; + errorHandler, + notFoundHandler, + asyncHandler, + // Export error classes + ValidationError, + AuthenticationError, + AuthorizationError, + NotFoundError, + ConflictError, + RateLimitError, + ServiceError, + DatabaseError +}; \ No newline at end of file diff --git a/src/middleware/logging.middleware.js b/src/middleware/logging.middleware.js index 1c07eef..e84b204 100644 --- a/src/middleware/logging.middleware.js +++ b/src/middleware/logging.middleware.js @@ -13,123 +13,123 @@ const { performance } = require('perf_hooks'); * @param {Function} next - Express next function */ function requestLogger(req, res, next) { - const startTime = performance.now(); - const correlationId = req.correlationId; + const startTime = performance.now(); + const correlationId = req.correlationId; - // Extract request information - const requestInfo = { - correlationId, - method: req.method, - url: req.originalUrl || req.url, - path: req.path, - query: Object.keys(req.query).length > 0 ? req.query : undefined, - ip: req.ip || req.connection.remoteAddress, - userAgent: req.get('User-Agent'), - contentType: req.get('Content-Type'), - contentLength: req.get('Content-Length'), - referrer: req.get('Referrer'), - origin: req.get('Origin'), - timestamp: new Date().toISOString(), - }; - - // Log request start - logger.info('Request started', requestInfo); - - // Store original methods to override - const originalSend = res.send; - const originalJson = res.json; - const originalEnd = res.end; - - let responseBody = null; - let responseSent = false; - - // Override res.send to capture response - res.send = function (data) { - if (!responseSent) { - responseBody = data; - logResponse(); - } - return originalSend.call(this, data); - }; - - // Override res.json to capture JSON response - res.json = function (data) { - if (!responseSent) { - responseBody = data; - logResponse(); - } - return originalJson.call(this, data); - }; - - // Override res.end to capture empty responses - res.end = function (data) { - if (!responseSent) { - responseBody = data; - logResponse(); - } - return originalEnd.call(this, data); - }; - - /** - * Log the response details - */ - function logResponse() { - if (responseSent) return; - responseSent = true; - - const endTime = performance.now(); - const duration = Math.round(endTime - startTime); - const statusCode = res.statusCode; - - const responseInfo = { - correlationId, - method: req.method, - url: req.originalUrl || req.url, - statusCode, - duration: `${duration}ms`, - contentLength: res.get('Content-Length'), - contentType: res.get('Content-Type'), - timestamp: new Date().toISOString(), + // Extract request information + const requestInfo = { + correlationId, + method: req.method, + url: req.originalUrl || req.url, + path: req.path, + query: Object.keys(req.query).length > 0 ? req.query : undefined, + ip: req.ip || req.connection.remoteAddress, + userAgent: req.get('User-Agent'), + contentType: req.get('Content-Type'), + contentLength: req.get('Content-Length'), + referrer: req.get('Referrer'), + origin: req.get('Origin'), + timestamp: new Date().toISOString() }; - // Add user information if available - if (req.user) { - responseInfo.userId = req.user.playerId || req.user.adminId; - responseInfo.userType = req.user.type; - responseInfo.username = req.user.username; + // Log request start + logger.info('Request started', requestInfo); + + // Store original methods to override + const originalSend = res.send; + const originalJson = res.json; + const originalEnd = res.end; + + let responseBody = null; + let responseSent = false; + + // Override res.send to capture response + res.send = function(data) { + if (!responseSent) { + responseBody = data; + logResponse(); + } + return originalSend.call(this, data); + }; + + // Override res.json to capture JSON response + res.json = function(data) { + if (!responseSent) { + responseBody = data; + logResponse(); + } + return originalJson.call(this, data); + }; + + // Override res.end to capture empty responses + res.end = function(data) { + if (!responseSent) { + responseBody = data; + logResponse(); + } + return originalEnd.call(this, data); + }; + + /** + * Log the response details + */ + function logResponse() { + if (responseSent) return; + responseSent = true; + + const endTime = performance.now(); + const duration = Math.round(endTime - startTime); + const statusCode = res.statusCode; + + const responseInfo = { + correlationId, + method: req.method, + url: req.originalUrl || req.url, + statusCode, + duration: `${duration}ms`, + contentLength: res.get('Content-Length'), + contentType: res.get('Content-Type'), + timestamp: new Date().toISOString() + }; + + // Add user information if available + if (req.user) { + responseInfo.userId = req.user.playerId || req.user.adminId; + responseInfo.userType = req.user.type; + responseInfo.username = req.user.username; + } + + // Determine log level based on status code + let logLevel = 'info'; + if (statusCode >= 400 && statusCode < 500) { + logLevel = 'warn'; + } else if (statusCode >= 500) { + logLevel = 'error'; + } + + // Add response body for errors (but sanitize sensitive data) + if (statusCode >= 400 && responseBody) { + responseInfo.responseBody = sanitizeResponseBody(responseBody); + } + + // Log slow requests as warnings + if (duration > 5000) { // 5 seconds + logLevel = 'warn'; + responseInfo.slow = true; + } + + logger[logLevel]('Request completed', responseInfo); + + // Log audit trail for sensitive operations + if (shouldAudit(req, statusCode)) { + logAuditTrail(req, res, duration, correlationId); + } + + // Track performance metrics + trackPerformanceMetrics(req, res, duration); } - // Determine log level based on status code - let logLevel = 'info'; - if (statusCode >= 400 && statusCode < 500) { - logLevel = 'warn'; - } else if (statusCode >= 500) { - logLevel = 'error'; - } - - // Add response body for errors (but sanitize sensitive data) - if (statusCode >= 400 && responseBody) { - responseInfo.responseBody = sanitizeResponseBody(responseBody); - } - - // Log slow requests as warnings - if (duration > 5000) { // 5 seconds - logLevel = 'warn'; - responseInfo.slow = true; - } - - logger[logLevel]('Request completed', responseInfo); - - // Log audit trail for sensitive operations - if (shouldAudit(req, statusCode)) { - logAuditTrail(req, res, duration, correlationId); - } - - // Track performance metrics - trackPerformanceMetrics(req, res, duration); - } - - next(); + next(); } /** @@ -138,47 +138,47 @@ function requestLogger(req, res, next) { * @returns {any} Sanitized response body */ function sanitizeResponseBody(responseBody) { - if (!responseBody) return responseBody; + if (!responseBody) return responseBody; - try { - let sanitized = responseBody; + try { + let sanitized = responseBody; + + // If it's a string, try to parse as JSON + if (typeof responseBody === 'string') { + try { + sanitized = JSON.parse(responseBody); + } catch { + return responseBody; // Return as-is if not JSON + } + } - // If it's a string, try to parse as JSON - if (typeof responseBody === 'string') { - try { - sanitized = JSON.parse(responseBody); - } catch { - return responseBody; // Return as-is if not JSON - } + // Remove sensitive fields + if (typeof sanitized === 'object') { + const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash']; + const cloned = JSON.parse(JSON.stringify(sanitized)); + + function removeSensitiveFields(obj) { + if (typeof obj !== 'object' || obj === null) return obj; + + Object.keys(obj).forEach(key => { + if (sensitiveFields.some(field => key.toLowerCase().includes(field))) { + obj[key] = '[REDACTED]'; + } else if (typeof obj[key] === 'object') { + removeSensitiveFields(obj[key]); + } + }); + + return obj; + } + + return removeSensitiveFields(cloned); + } + + return sanitized; + + } catch (error) { + return '[SANITIZATION_ERROR]'; } - - // Remove sensitive fields - if (typeof sanitized === 'object') { - const sensitiveFields = ['password', 'token', 'secret', 'key', 'hash']; - const cloned = JSON.parse(JSON.stringify(sanitized)); - - function removeSensitiveFields(obj) { - if (typeof obj !== 'object' || obj === null) return obj; - - Object.keys(obj).forEach(key => { - if (sensitiveFields.some(field => key.toLowerCase().includes(field))) { - obj[key] = '[REDACTED]'; - } else if (typeof obj[key] === 'object') { - removeSensitiveFields(obj[key]); - } - }); - - return obj; - } - - return removeSensitiveFields(cloned); - } - - return sanitized; - - } catch (error) { - return '[SANITIZATION_ERROR]'; - } } /** @@ -188,35 +188,35 @@ function sanitizeResponseBody(responseBody) { * @returns {boolean} True if should audit */ function shouldAudit(req, statusCode) { - // Audit admin actions - if (req.user?.type === 'admin') { - return true; - } + // Audit admin actions + if (req.user?.type === 'admin') { + return true; + } - // Audit authentication attempts - if (req.path.includes('/auth/') || req.path.includes('/login')) { - return true; - } + // Audit authentication attempts + if (req.path.includes('/auth/') || req.path.includes('/login')) { + return true; + } - // Audit failed requests - if (statusCode >= 400) { - return true; - } + // Audit failed requests + if (statusCode >= 400) { + return true; + } - // Audit sensitive game actions - const sensitiveActions = [ - '/colonies', - '/fleets', - '/research', - '/messages', - '/profile', - ]; + // Audit sensitive game actions + const sensitiveActions = [ + '/colonies', + '/fleets', + '/research', + '/messages', + '/profile' + ]; - if (sensitiveActions.some(action => req.path.includes(action)) && req.method !== 'GET') { - return true; - } + if (sensitiveActions.some(action => req.path.includes(action)) && req.method !== 'GET') { + return true; + } - return false; + return false; } /** @@ -227,36 +227,36 @@ function shouldAudit(req, statusCode) { * @param {string} correlationId - Request correlation ID */ function logAuditTrail(req, res, duration, correlationId) { - const auditInfo = { - correlationId, - event: 'api_request', - method: req.method, - path: req.path, - statusCode: res.statusCode, - duration: `${duration}ms`, - ip: req.ip, - userAgent: req.get('User-Agent'), - timestamp: new Date().toISOString(), - }; + const auditInfo = { + correlationId, + event: 'api_request', + method: req.method, + path: req.path, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip, + userAgent: req.get('User-Agent'), + timestamp: new Date().toISOString() + }; - // Add user information - if (req.user) { - auditInfo.userId = req.user.playerId || req.user.adminId; - auditInfo.userType = req.user.type; - auditInfo.username = req.user.username; - } + // Add user information + if (req.user) { + auditInfo.userId = req.user.playerId || req.user.adminId; + auditInfo.userType = req.user.type; + auditInfo.username = req.user.username; + } - // Add request parameters for POST/PUT/PATCH requests (sanitized) - if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) { - auditInfo.requestBody = sanitizeRequestBody(req.body); - } + // Add request parameters for POST/PUT/PATCH requests (sanitized) + if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body) { + auditInfo.requestBody = sanitizeRequestBody(req.body); + } - // Add query parameters - if (Object.keys(req.query).length > 0) { - auditInfo.queryParams = req.query; - } + // Add query parameters + if (Object.keys(req.query).length > 0) { + auditInfo.queryParams = req.query; + } - logger.audit('Audit trail', auditInfo); + logger.audit('Audit trail', auditInfo); } /** @@ -265,22 +265,22 @@ function logAuditTrail(req, res, duration, correlationId) { * @returns {Object} Sanitized request body */ function sanitizeRequestBody(body) { - if (!body || typeof body !== 'object') return body; + if (!body || typeof body !== 'object') return body; - try { - const sensitiveFields = ['password', 'oldPassword', 'newPassword', 'token', 'secret']; - const cloned = JSON.parse(JSON.stringify(body)); + try { + const sensitiveFields = ['password', 'oldPassword', 'newPassword', 'token', 'secret']; + const cloned = JSON.parse(JSON.stringify(body)); + + sensitiveFields.forEach(field => { + if (cloned[field]) { + cloned[field] = '[REDACTED]'; + } + }); - sensitiveFields.forEach(field => { - if (cloned[field]) { - cloned[field] = '[REDACTED]'; - } - }); - - return cloned; - } catch { - return '[SANITIZATION_ERROR]'; - } + return cloned; + } catch { + return '[SANITIZATION_ERROR]'; + } } /** @@ -290,36 +290,36 @@ function sanitizeRequestBody(body) { * @param {number} duration - Request duration in milliseconds */ function trackPerformanceMetrics(req, res, duration) { - // Only track metrics for non-health check endpoints - if (req.path === '/health') return; + // Only track metrics for non-health check endpoints + if (req.path === '/health') return; - const metrics = { - endpoint: `${req.method} ${req.route?.path || req.path}`, - duration, - statusCode: res.statusCode, - timestamp: Date.now(), - }; + const metrics = { + endpoint: `${req.method} ${req.route?.path || req.path}`, + duration, + statusCode: res.statusCode, + timestamp: Date.now() + }; - // Log slow requests - if (duration > 1000) { // 1 second - logger.warn('Slow request detected', { - correlationId: req.correlationId, - ...metrics, - threshold: '1000ms', - }); - } + // Log slow requests + if (duration > 1000) { // 1 second + logger.warn('Slow request detected', { + correlationId: req.correlationId, + ...metrics, + threshold: '1000ms' + }); + } - // Log very slow requests as errors - if (duration > 10000) { // 10 seconds - logger.error('Very slow request detected', { - correlationId: req.correlationId, - ...metrics, - threshold: '10000ms', - }); - } + // Log very slow requests as errors + if (duration > 10000) { // 10 seconds + logger.error('Very slow request detected', { + correlationId: req.correlationId, + ...metrics, + threshold: '10000ms' + }); + } - // TODO: Send metrics to monitoring system (Prometheus, DataDog, etc.) - // This would integrate with your monitoring infrastructure + // TODO: Send metrics to monitoring system (Prometheus, DataDog, etc.) + // This would integrate with your monitoring infrastructure } /** @@ -328,15 +328,15 @@ function trackPerformanceMetrics(req, res, duration) { * @returns {Function} Middleware function */ function skipLogging(skipPaths = ['/health', '/favicon.ico']) { - return (req, res, next) => { - const shouldSkip = skipPaths.some(path => req.path === path); + return (req, res, next) => { + const shouldSkip = skipPaths.some(path => req.path === path); + + if (shouldSkip) { + return next(); + } - if (shouldSkip) { - return next(); - } - - return requestLogger(req, res, next); - }; + return requestLogger(req, res, next); + }; } /** @@ -347,25 +347,25 @@ function skipLogging(skipPaths = ['/health', '/favicon.ico']) { * @param {Function} next - Express next function */ function errorLogger(error, req, res, next) { - logger.error('Unhandled request error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - method: req.method, - url: req.originalUrl, - ip: req.ip, - userAgent: req.get('User-Agent'), - userId: req.user?.playerId || req.user?.adminId, - userType: req.user?.type, - }); + logger.error('Unhandled request error', { + correlationId: req.correlationId, + error: error.message, + stack: error.stack, + method: req.method, + url: req.originalUrl, + ip: req.ip, + userAgent: req.get('User-Agent'), + userId: req.user?.playerId || req.user?.adminId, + userType: req.user?.type + }); - next(error); + next(error); } module.exports = { - requestLogger, - skipLogging, - errorLogger, - sanitizeResponseBody, - sanitizeRequestBody, -}; + requestLogger, + skipLogging, + errorLogger, + sanitizeResponseBody, + sanitizeRequestBody +}; \ No newline at end of file diff --git a/src/middleware/rateLimit.middleware.js b/src/middleware/rateLimit.middleware.js index c3b527a..d066dfe 100644 --- a/src/middleware/rateLimit.middleware.js +++ b/src/middleware/rateLimit.middleware.js @@ -9,65 +9,65 @@ const logger = require('../utils/logger'); // Rate limiting configuration const RATE_LIMIT_CONFIG = { - // Global API rate limits - global: { - windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes - max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 1000, // 1000 requests per window - standardHeaders: true, - legacyHeaders: false, - skipSuccessfulRequests: false, - skipFailedRequests: false, - }, + // Global API rate limits + global: { + windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes + max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 1000, // 1000 requests per window + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: false, + skipFailedRequests: false + }, - // Authentication endpoints (more restrictive) - auth: { - windowMs: 15 * 60 * 1000, // 15 minutes - max: 10, // 10 attempts per window - standardHeaders: true, - legacyHeaders: false, - skipSuccessfulRequests: true, // Don't count successful logins - skipFailedRequests: false, - }, + // Authentication endpoints (more restrictive) + auth: { + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, // 10 attempts per window + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: true, // Don't count successful logins + skipFailedRequests: false + }, - // Player API endpoints - player: { - windowMs: 1 * 60 * 1000, // 1 minute - max: 120, // 120 requests per minute - standardHeaders: true, - legacyHeaders: false, - skipSuccessfulRequests: false, - skipFailedRequests: false, - }, + // Player API endpoints + player: { + windowMs: 1 * 60 * 1000, // 1 minute + max: 120, // 120 requests per minute + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: false, + skipFailedRequests: false + }, - // Admin API endpoints (more lenient for legitimate admin users) - admin: { - windowMs: 1 * 60 * 1000, // 1 minute - max: 300, // 300 requests per minute - standardHeaders: true, - legacyHeaders: false, - skipSuccessfulRequests: false, - skipFailedRequests: false, - }, + // Admin API endpoints (more lenient for legitimate admin users) + admin: { + windowMs: 1 * 60 * 1000, // 1 minute + max: 300, // 300 requests per minute + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: false, + skipFailedRequests: false + }, - // Game action endpoints (prevent spam) - gameAction: { - windowMs: 30 * 1000, // 30 seconds - max: 30, // 30 actions per 30 seconds - standardHeaders: true, - legacyHeaders: false, - skipSuccessfulRequests: false, - skipFailedRequests: true, - }, + // Game action endpoints (prevent spam) + gameAction: { + windowMs: 30 * 1000, // 30 seconds + max: 30, // 30 actions per 30 seconds + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: false, + skipFailedRequests: true + }, - // Message sending (prevent spam) - messaging: { - windowMs: 5 * 60 * 1000, // 5 minutes - max: 10, // 10 messages per 5 minutes - standardHeaders: true, - legacyHeaders: false, - skipSuccessfulRequests: false, - skipFailedRequests: true, - }, + // Message sending (prevent spam) + messaging: { + windowMs: 5 * 60 * 1000, // 5 minutes + max: 10, // 10 messages per 5 minutes + standardHeaders: true, + legacyHeaders: false, + skipSuccessfulRequests: false, + skipFailedRequests: true + } }; /** @@ -75,40 +75,34 @@ const RATE_LIMIT_CONFIG = { * @returns {Object|null} Redis store or null if Redis unavailable */ function createRedisStore() { - // Check if Redis is disabled first - if (process.env.DISABLE_REDIS === 'true') { - logger.info('Redis disabled for rate limiting, using memory store'); - return null; - } - - try { - const redis = getRedisClient(); - if (!redis) { - logger.warn('Redis not available for rate limiting, using memory store'); - return null; - } - - // Create Redis store for express-rate-limit try { - const { RedisStore } = require('rate-limit-redis'); + const redis = getRedisClient(); + if (!redis) { + logger.warn('Redis not available for rate limiting, using memory store'); + return null; + } + + // Create Redis store for express-rate-limit + try { + const { RedisStore } = require('rate-limit-redis'); + + return new RedisStore({ + sendCommand: (...args) => redis.sendCommand(args), + prefix: 'rl:' // Rate limit prefix + }); + } catch (error) { + logger.warn('Failed to create RedisStore, falling back to memory store', { + error: error.message + }); + return null; + } - return new RedisStore({ - sendCommand: (...args) => redis.sendCommand(args), - prefix: 'rl:', // Rate limit prefix - }); } catch (error) { - logger.warn('Failed to create RedisStore, falling back to memory store', { - error: error.message, - }); - return null; + logger.warn('Failed to create Redis store for rate limiting', { + error: error.message + }); + return null; } - - } catch (error) { - logger.warn('Failed to create Redis store for rate limiting', { - error: error.message, - }); - return null; - } } /** @@ -117,11 +111,11 @@ function createRedisStore() { * @returns {Function} Key generator function */ function createKeyGenerator(prefix = 'global') { - return (req) => { - const ip = req.ip || req.connection.remoteAddress || 'unknown'; - const userId = req.user?.playerId || req.user?.adminId || 'anonymous'; - return `${prefix}:${userId}:${ip}`; - }; + return (req) => { + const ip = req.ip || req.connection.remoteAddress || 'unknown'; + const userId = req.user?.playerId || req.user?.adminId || 'anonymous'; + return `${prefix}:${userId}:${ip}`; + }; } /** @@ -130,32 +124,32 @@ function createKeyGenerator(prefix = 'global') { * @returns {Function} Rate limit handler function */ function createRateLimitHandler(type) { - return (req, res) => { - const correlationId = req.correlationId; - const ip = req.ip || req.connection.remoteAddress; - const userId = req.user?.playerId || req.user?.adminId; - const userType = req.user?.type || 'anonymous'; + return (req, res) => { + const correlationId = req.correlationId; + const ip = req.ip || req.connection.remoteAddress; + const userId = req.user?.playerId || req.user?.adminId; + const userType = req.user?.type || 'anonymous'; - logger.warn('Rate limit exceeded', { - correlationId, - type, - ip, - userId, - userType, - path: req.path, - method: req.method, - userAgent: req.get('User-Agent'), - retryAfter: res.get('Retry-After'), - }); + logger.warn('Rate limit exceeded', { + correlationId, + type, + ip, + userId, + userType, + path: req.path, + method: req.method, + userAgent: req.get('User-Agent'), + retryAfter: res.get('Retry-After') + }); - return res.status(429).json({ - error: 'Too Many Requests', - message: 'Rate limit exceeded. Please try again later.', - type, - retryAfter: res.get('Retry-After'), - correlationId, - }); - }; + return res.status(429).json({ + error: 'Too Many Requests', + message: 'Rate limit exceeded. Please try again later.', + type: type, + retryAfter: res.get('Retry-After'), + correlationId + }); + }; } /** @@ -165,31 +159,31 @@ function createRateLimitHandler(type) { * @returns {Function} Skip function */ function createSkipFunction(skipPaths = [], skipIPs = []) { - return (req) => { - const ip = req.ip || req.connection.remoteAddress; + return (req) => { + const ip = req.ip || req.connection.remoteAddress; + + // Skip health checks + if (req.path === '/health' || req.path === '/api/health') { + return true; + } - // Skip health checks - if (req.path === '/health' || req.path === '/api/health') { - return true; - } + // Skip specified paths + if (skipPaths.some(path => req.path.startsWith(path))) { + return true; + } - // Skip specified paths - if (skipPaths.some(path => req.path.startsWith(path))) { - return true; - } + // Skip specified IPs (for development/testing) + if (skipIPs.includes(ip)) { + return true; + } - // Skip specified IPs (for development/testing) - if (skipIPs.includes(ip)) { - return true; - } + // Skip if rate limiting is disabled + if (process.env.DISABLE_RATE_LIMITING === 'true') { + return true; + } - // Skip if rate limiting is disabled - if (process.env.DISABLE_RATE_LIMITING === 'true') { - return true; - } - - return false; - }; + return false; + }; } /** @@ -199,40 +193,40 @@ function createSkipFunction(skipPaths = [], skipIPs = []) { * @returns {Function} Rate limiter middleware */ function createRateLimiter(type, customConfig = {}) { - const config = { ...RATE_LIMIT_CONFIG[type], ...customConfig }; - const store = createRedisStore(); + const config = { ...RATE_LIMIT_CONFIG[type], ...customConfig }; + const store = createRedisStore(); - const rateLimiter = rateLimit({ - ...config, - store, - keyGenerator: createKeyGenerator(type), - handler: createRateLimitHandler(type), - skip: createSkipFunction(), - // Note: onLimitReached is deprecated in express-rate-limit v7 - // Removed for compatibility - }); + const rateLimiter = rateLimit({ + ...config, + store, + keyGenerator: createKeyGenerator(type), + handler: createRateLimitHandler(type), + skip: createSkipFunction(), + // Note: onLimitReached is deprecated in express-rate-limit v7 + // Removed for compatibility + }); - // Log rate limiter creation - logger.info('Rate limiter created', { - type, - windowMs: config.windowMs, - max: config.max, - useRedis: !!store, - }); + // Log rate limiter creation + logger.info('Rate limiter created', { + type, + windowMs: config.windowMs, + max: config.max, + useRedis: !!store + }); - return rateLimiter; + return rateLimiter; } /** * Pre-configured rate limiters */ const rateLimiters = { - global: createRateLimiter('global'), - auth: createRateLimiter('auth'), - player: createRateLimiter('player'), - admin: createRateLimiter('admin'), - gameAction: createRateLimiter('gameAction'), - messaging: createRateLimiter('messaging'), + global: createRateLimiter('global'), + auth: createRateLimiter('auth'), + player: createRateLimiter('player'), + admin: createRateLimiter('admin'), + gameAction: createRateLimiter('gameAction'), + messaging: createRateLimiter('messaging') }; /** @@ -242,12 +236,12 @@ const rateLimiters = { * @param {Function} next - Express next function */ function addRateLimitHeaders(req, res, next) { - // Add custom headers for client information - res.set({ - 'X-RateLimit-Policy': 'See API documentation for rate limiting details', - }); + // Add custom headers for client information + res.set({ + 'X-RateLimit-Policy': 'See API documentation for rate limiting details' + }); - next(); + next(); } /** @@ -257,42 +251,42 @@ function addRateLimitHeaders(req, res, next) { * @returns {Function} WebSocket rate limiter function */ function createWebSocketRateLimiter(maxConnections = 10, windowMs = 60000) { - const connections = new Map(); + const connections = new Map(); - return (socket, next) => { - const ip = socket.handshake.address; - const now = Date.now(); + return (socket, next) => { + const ip = socket.handshake.address; + const now = Date.now(); - // Clean up old connections - if (connections.has(ip)) { - const connectionTimes = connections.get(ip).filter(time => now - time < windowMs); - connections.set(ip, connectionTimes); - } + // Clean up old connections + if (connections.has(ip)) { + const connectionTimes = connections.get(ip).filter(time => now - time < windowMs); + connections.set(ip, connectionTimes); + } - // Check rate limit - const currentConnections = connections.get(ip) || []; - if (currentConnections.length >= maxConnections) { - logger.warn('WebSocket connection rate limit exceeded', { - ip, - currentConnections: currentConnections.length, - maxConnections, - }); + // Check rate limit + const currentConnections = connections.get(ip) || []; + if (currentConnections.length >= maxConnections) { + logger.warn('WebSocket connection rate limit exceeded', { + ip, + currentConnections: currentConnections.length, + maxConnections + }); - return next(new Error('Connection rate limit exceeded')); - } + return next(new Error('Connection rate limit exceeded')); + } - // Add current connection - currentConnections.push(now); - connections.set(ip, currentConnections); + // Add current connection + currentConnections.push(now); + connections.set(ip, currentConnections); - logger.debug('WebSocket connection allowed', { - ip, - connections: currentConnections.length, - maxConnections, - }); + logger.debug('WebSocket connection allowed', { + ip, + connections: currentConnections.length, + maxConnections + }); - next(); - }; + next(); + }; } /** @@ -302,25 +296,25 @@ function createWebSocketRateLimiter(maxConnections = 10, windowMs = 60000) { * @param {Function} next - Express next function */ function dynamicRateLimit(req, res, next) { - const userType = req.user?.type; + const userType = req.user?.type; + + let limiter; + if (userType === 'admin') { + limiter = rateLimiters.admin; + } else if (userType === 'player') { + limiter = rateLimiters.player; + } else { + limiter = rateLimiters.global; + } - let limiter; - if (userType === 'admin') { - limiter = rateLimiters.admin; - } else if (userType === 'player') { - limiter = rateLimiters.player; - } else { - limiter = rateLimiters.global; - } - - return limiter(req, res, next); + return limiter(req, res, next); } module.exports = { - rateLimiters, - createRateLimiter, - createWebSocketRateLimiter, - addRateLimitHeaders, - dynamicRateLimit, - RATE_LIMIT_CONFIG, -}; + rateLimiters, + createRateLimiter, + createWebSocketRateLimiter, + addRateLimitHeaders, + dynamicRateLimit, + RATE_LIMIT_CONFIG +}; \ No newline at end of file diff --git a/src/middleware/request-logger.js b/src/middleware/request-logger.js index cb525df..e0ec59f 100644 --- a/src/middleware/request-logger.js +++ b/src/middleware/request-logger.js @@ -12,10 +12,10 @@ const logger = require('../utils/logger'); function requestLogger(req, res, next) { // Generate correlation ID for request tracking req.correlationId = uuidv4(); - + // Capture start time const startTime = Date.now(); - + // Extract user info if available const getUserInfo = () => { if (req.user) { @@ -42,7 +42,7 @@ function requestLogger(req, res, next) { const originalJson = res.json; res.json = function (body) { const duration = Date.now() - startTime; - + // Log request completion logger.info('Request completed', { correlationId: req.correlationId, @@ -70,7 +70,7 @@ function requestLogger(req, res, next) { const originalSend = res.send; res.send = function (body) { const duration = Date.now() - startTime; - + // Only log if not already logged by res.json if (!res.jsonLogged) { logger.info('Request completed', { @@ -89,4 +89,4 @@ function requestLogger(req, res, next) { next(); } -module.exports = requestLogger; +module.exports = requestLogger; \ No newline at end of file diff --git a/src/middleware/security.middleware.js b/src/middleware/security.middleware.js deleted file mode 100644 index 3d669c1..0000000 --- a/src/middleware/security.middleware.js +++ /dev/null @@ -1,485 +0,0 @@ -/** - * Enhanced Security Middleware - * Provides advanced security controls including account lockout, rate limiting, and token validation - */ - -const logger = require('../utils/logger'); -const { verifyPlayerToken, extractTokenFromHeader } = require('../utils/jwt'); -const TokenService = require('../services/auth/TokenService'); -const { generateRateLimitKey } = require('../utils/security'); -const redis = require('../utils/redis'); - -class SecurityMiddleware { - constructor() { - this.tokenService = new TokenService(); - this.redisClient = redis; - } - - /** - * Enhanced authentication middleware with token blacklist checking - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next function - */ - async enhancedAuth(req, res, next) { - try { - const correlationId = req.correlationId; - const authHeader = req.headers.authorization; - - if (!authHeader) { - logger.warn('Authentication required - no authorization header', { - correlationId, - path: req.path, - method: req.method, - }); - - return res.status(401).json({ - success: false, - message: 'Authentication required', - correlationId, - }); - } - - const token = extractTokenFromHeader(authHeader); - if (!token) { - logger.warn('Authentication failed - invalid authorization header format', { - correlationId, - authHeader: authHeader.substring(0, 20) + '...', - }); - - return res.status(401).json({ - success: false, - message: 'Invalid authorization header format', - correlationId, - }); - } - - // Check if token is blacklisted - const isBlacklisted = await this.tokenService.isTokenBlacklisted(token); - if (isBlacklisted) { - logger.warn('Authentication failed - token is blacklisted', { - correlationId, - tokenPrefix: token.substring(0, 20) + '...', - }); - - return res.status(401).json({ - success: false, - message: 'Token has been revoked', - correlationId, - }); - } - - // Verify token - const decoded = verifyPlayerToken(token); - - // Add user info to request - req.user = decoded; - req.accessToken = token; - - logger.info('Authentication successful', { - correlationId, - playerId: decoded.playerId, - username: decoded.username, - }); - - next(); - - } catch (error) { - logger.warn('Authentication failed', { - correlationId: req.correlationId, - error: error.message, - path: req.path, - method: req.method, - }); - - if (error.message === 'Token expired') { - return res.status(401).json({ - success: false, - message: 'Token expired', - code: 'TOKEN_EXPIRED', - correlationId: req.correlationId, - }); - } - - return res.status(401).json({ - success: false, - message: 'Invalid or expired token', - correlationId: req.correlationId, - }); - } - } - - /** - * Account lockout protection middleware - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next function - */ - async accountLockoutProtection(req, res, next) { - try { - const correlationId = req.correlationId; - const email = req.body.email; - const ipAddress = req.ip || req.connection.remoteAddress; - - if (!email) { - return next(); - } - - // Check account lockout by email - const emailLockout = await this.tokenService.isAccountLocked(email); - if (emailLockout.isLocked) { - logger.warn('Login blocked - account locked', { - correlationId, - email, - lockedUntil: emailLockout.expiresAt, - reason: emailLockout.reason, - }); - - return res.status(423).json({ - success: false, - message: `Account temporarily locked due to security concerns. Try again after ${emailLockout.expiresAt.toLocaleString()}`, - code: 'ACCOUNT_LOCKED', - correlationId, - retryAfter: emailLockout.expiresAt.toISOString(), - }); - } - - // Check IP-based lockout - const ipLockout = await this.tokenService.isAccountLocked(ipAddress); - if (ipLockout.isLocked) { - logger.warn('Login blocked - IP locked', { - correlationId, - ipAddress, - lockedUntil: ipLockout.expiresAt, - reason: ipLockout.reason, - }); - - return res.status(423).json({ - success: false, - message: 'Too many failed attempts from this location. Please try again later.', - code: 'IP_LOCKED', - correlationId, - retryAfter: ipLockout.expiresAt.toISOString(), - }); - } - - next(); - - } catch (error) { - logger.error('Account lockout protection error', { - correlationId: req.correlationId, - error: error.message, - }); - // Continue on error to avoid blocking legitimate users - next(); - } - } - - /** - * Rate limiting middleware for specific actions - * @param {Object} options - Rate limiting options - * @param {number} options.maxRequests - Maximum requests per window - * @param {number} options.windowMinutes - Time window in minutes - * @param {string} options.action - Action identifier - * @param {Function} options.keyGenerator - Custom key generator function - */ - rateLimiter(options = {}) { - const defaults = { - maxRequests: 5, - windowMinutes: 15, - action: 'generic', - keyGenerator: (req) => req.ip || 'unknown', - }; - - const config = { ...defaults, ...options }; - - return async (req, res, next) => { - try { - const correlationId = req.correlationId; - const identifier = config.keyGenerator(req); - const rateLimitKey = generateRateLimitKey(identifier, config.action, config.windowMinutes); - - // Get current count - const currentCount = await this.redisClient.incr(rateLimitKey); - - if (currentCount === 1) { - // Set expiration on first request - await this.redisClient.expire(rateLimitKey, config.windowMinutes * 60); - } - - // Check if limit exceeded - if (currentCount > config.maxRequests) { - logger.warn('Rate limit exceeded', { - correlationId, - identifier, - action: config.action, - attempts: currentCount, - maxRequests: config.maxRequests, - windowMinutes: config.windowMinutes, - }); - - return res.status(429).json({ - success: false, - message: `Too many ${config.action} requests. Please try again later.`, - code: 'RATE_LIMIT_EXCEEDED', - correlationId, - retryAfter: config.windowMinutes * 60, - }); - } - - // Add rate limit headers - res.set({ - 'X-RateLimit-Limit': config.maxRequests, - 'X-RateLimit-Remaining': Math.max(0, config.maxRequests - currentCount), - 'X-RateLimit-Reset': new Date(Date.now() + (config.windowMinutes * 60 * 1000)).toISOString(), - }); - - next(); - - } catch (error) { - logger.error('Rate limiter error', { - correlationId: req.correlationId, - error: error.message, - action: config.action, - }); - // Continue on error to avoid blocking legitimate users - next(); - } - }; - } - - /** - * Password strength validation middleware - * @param {string} passwordField - Field name containing password (default: 'password') - */ - passwordStrengthValidator(passwordField = 'password') { - return (req, res, next) => { - const correlationId = req.correlationId; - const password = req.body[passwordField]; - - if (!password) { - return next(); - } - - const { validatePasswordStrength } = require('../utils/security'); - const validation = validatePasswordStrength(password); - - if (!validation.isValid) { - logger.warn('Password strength validation failed', { - correlationId, - errors: validation.errors, - strength: validation.strength, - }); - - return res.status(400).json({ - success: false, - message: 'Password does not meet security requirements', - code: 'WEAK_PASSWORD', - correlationId, - details: { - errors: validation.errors, - requirements: validation.requirements, - strength: validation.strength, - }, - }); - } - - // Add password strength info to request for logging - req.passwordStrength = validation.strength; - next(); - }; - } - - /** - * Email verification requirement middleware - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next function - */ - async requireEmailVerification(req, res, next) { - try { - const correlationId = req.correlationId; - const playerId = req.user?.playerId; - - if (!playerId) { - return next(); - } - - // Get player verification status - const db = require('../database/connection'); - const player = await db('players') - .select('email_verified') - .where('id', playerId) - .first(); - - if (!player) { - logger.warn('Email verification check - player not found', { - correlationId, - playerId, - }); - - return res.status(404).json({ - success: false, - message: 'Player not found', - correlationId, - }); - } - - // TODO: Re-enable email verification when email system is ready - // if (!player.email_verified) { - // logger.warn('Email verification required', { - // correlationId, - // playerId, - // }); - - // return res.status(403).json({ - // success: false, - // message: 'Email verification required to access this resource', - // code: 'EMAIL_NOT_VERIFIED', - // correlationId, - // }); - // } - - next(); - - } catch (error) { - logger.error('Email verification check error', { - correlationId: req.correlationId, - playerId: req.user?.playerId, - error: error.message, - }); - - return res.status(500).json({ - success: false, - message: 'Internal server error', - correlationId: req.correlationId, - }); - } - } - - /** - * Security headers middleware - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next function - */ - securityHeaders(req, res, next) { - // Add security headers - res.set({ - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY', - 'X-XSS-Protection': '1; mode=block', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - 'Permissions-Policy': 'geolocation=(), microphone=(), camera=()', - }); - - // Add HSTS header in production - if (process.env.NODE_ENV === 'production') { - res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); - } - - next(); - } - - /** - * Input sanitization middleware - * @param {Array} fields - Fields to sanitize - */ - sanitizeInput(fields = []) { - return (req, res, next) => { - const { sanitizeInput } = require('../utils/security'); - - for (const field of fields) { - if (req.body[field] && typeof req.body[field] === 'string') { - req.body[field] = sanitizeInput(req.body[field], { - trim: true, - maxLength: 1000, - stripHtml: true, - }); - } - } - - next(); - }; - } - - /** - * CSRF protection middleware - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @param {Function} next - Express next function - */ - async csrfProtection(req, res, next) { - // Skip CSRF for GET requests and API authentication - if (req.method === 'GET' || req.path.startsWith('/api/auth/')) { - return next(); - } - - try { - const correlationId = req.correlationId; - const csrfToken = req.headers['x-csrf-token'] || req.body._csrf; - const sessionId = req.session?.id || req.user?.playerId?.toString(); - - if (!csrfToken || !sessionId) { - logger.warn('CSRF protection - missing token or session', { - correlationId, - hasToken: !!csrfToken, - hasSession: !!sessionId, - }); - - return res.status(403).json({ - success: false, - message: 'CSRF token required', - code: 'CSRF_TOKEN_MISSING', - correlationId, - }); - } - - const { verifyCSRFToken } = require('../utils/security'); - const isValid = verifyCSRFToken(csrfToken, sessionId); - - if (!isValid) { - logger.warn('CSRF protection - invalid token', { - correlationId, - sessionId, - }); - - return res.status(403).json({ - success: false, - message: 'Invalid CSRF token', - code: 'CSRF_TOKEN_INVALID', - correlationId, - }); - } - - next(); - - } catch (error) { - logger.error('CSRF protection error', { - correlationId: req.correlationId, - error: error.message, - }); - - return res.status(403).json({ - success: false, - message: 'CSRF validation failed', - correlationId: req.correlationId, - }); - } - } -} - -// Create singleton instance -const securityMiddleware = new SecurityMiddleware(); - -// Export middleware functions bound to the instance -module.exports = { - enhancedAuth: securityMiddleware.enhancedAuth.bind(securityMiddleware), - accountLockoutProtection: securityMiddleware.accountLockoutProtection.bind(securityMiddleware), - rateLimiter: securityMiddleware.rateLimiter.bind(securityMiddleware), - passwordStrengthValidator: securityMiddleware.passwordStrengthValidator.bind(securityMiddleware), - requireEmailVerification: securityMiddleware.requireEmailVerification.bind(securityMiddleware), - securityHeaders: securityMiddleware.securityHeaders.bind(securityMiddleware), - sanitizeInput: securityMiddleware.sanitizeInput.bind(securityMiddleware), - csrfProtection: securityMiddleware.csrfProtection.bind(securityMiddleware), -}; \ No newline at end of file diff --git a/src/middleware/validation.middleware.js b/src/middleware/validation.middleware.js index 4beaa34..519f63e 100644 --- a/src/middleware/validation.middleware.js +++ b/src/middleware/validation.middleware.js @@ -13,254 +13,254 @@ const logger = require('../utils/logger'); * @returns {Function} Express middleware function */ function validateRequest(schema, source = 'body') { - return (req, res, next) => { - try { - const correlationId = req.correlationId; - let dataToValidate; + return (req, res, next) => { + try { + const correlationId = req.correlationId; + let dataToValidate; - // Get data based on source - switch (source) { - case 'body': - dataToValidate = req.body; - break; - case 'params': - dataToValidate = req.params; - break; - case 'query': - dataToValidate = req.query; - break; - case 'headers': - dataToValidate = req.headers; - break; - default: - logger.error('Invalid validation source specified', { - correlationId, - source, - path: req.path, - }); - return res.status(500).json({ - error: 'Internal server error', - message: 'Invalid validation configuration', - correlationId, - }); - } + // Get data based on source + switch (source) { + case 'body': + dataToValidate = req.body; + break; + case 'params': + dataToValidate = req.params; + break; + case 'query': + dataToValidate = req.query; + break; + case 'headers': + dataToValidate = req.headers; + break; + default: + logger.error('Invalid validation source specified', { + correlationId, + source, + path: req.path + }); + return res.status(500).json({ + error: 'Internal server error', + message: 'Invalid validation configuration', + correlationId + }); + } - // Perform validation - const { error, value } = schema.validate(dataToValidate, { - abortEarly: false, // Return all validation errors - stripUnknown: true, // Remove unknown properties - convert: true, // Convert values to correct types - }); + // Perform validation + const { error, value } = schema.validate(dataToValidate, { + abortEarly: false, // Return all validation errors + stripUnknown: true, // Remove unknown properties + convert: true // Convert values to correct types + }); - if (error) { - const validationErrors = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - value: detail.context?.value, - })); + if (error) { + const validationErrors = error.details.map(detail => ({ + field: detail.path.join('.'), + message: detail.message, + value: detail.context?.value + })); - logger.warn('Request validation failed', { - correlationId, - source, - path: req.path, - method: req.method, - errors: validationErrors, - originalData: JSON.stringify(dataToValidate), - }); + logger.warn('Request validation failed', { + correlationId, + source, + path: req.path, + method: req.method, + errors: validationErrors, + originalData: JSON.stringify(dataToValidate) + }); - return res.status(400).json({ - error: 'Validation failed', - message: 'Request data is invalid', - details: validationErrors, - correlationId, - }); - } + return res.status(400).json({ + error: 'Validation failed', + message: 'Request data is invalid', + details: validationErrors, + correlationId + }); + } - // Replace the original data with validated/sanitized data - switch (source) { - case 'body': - req.body = value; - break; - case 'params': - req.params = value; - break; - case 'query': - req.query = value; - break; - case 'headers': - req.headers = value; - break; - } + // Replace the original data with validated/sanitized data + switch (source) { + case 'body': + req.body = value; + break; + case 'params': + req.params = value; + break; + case 'query': + req.query = value; + break; + case 'headers': + req.headers = value; + break; + } - logger.debug('Request validation passed', { - correlationId, - source, - path: req.path, - }); + logger.debug('Request validation passed', { + correlationId, + source, + path: req.path + }); - next(); + next(); - } catch (error) { - logger.error('Validation middleware error', { - correlationId: req.correlationId, - error: error.message, - stack: error.stack, - source, - }); + } catch (error) { + logger.error('Validation middleware error', { + correlationId: req.correlationId, + error: error.message, + stack: error.stack, + source + }); - return res.status(500).json({ - error: 'Internal server error', - message: 'Validation processing failed', - correlationId: req.correlationId, - }); - } - }; + return res.status(500).json({ + error: 'Internal server error', + message: 'Validation processing failed', + correlationId: req.correlationId + }); + } + }; } /** * Common validation schemas */ const commonSchemas = { - // Player ID parameter validation - playerId: Joi.object({ - playerId: Joi.number().integer().min(1).required(), - }), + // Player ID parameter validation + playerId: Joi.object({ + playerId: Joi.number().integer().min(1).required() + }), - // Pagination query validation - pagination: Joi.object({ - page: Joi.number().integer().min(1).default(1), - limit: Joi.number().integer().min(1).max(100).default(20), - sortBy: Joi.string().valid('created_at', 'updated_at', 'name', 'id').default('created_at'), - sortOrder: Joi.string().valid('asc', 'desc').default('desc'), - }), + // Pagination query validation + pagination: Joi.object({ + page: Joi.number().integer().min(1).default(1), + limit: Joi.number().integer().min(1).max(100).default(20), + sortBy: Joi.string().valid('created_at', 'updated_at', 'name', 'id').default('created_at'), + sortOrder: Joi.string().valid('asc', 'desc').default('desc') + }), - // Player registration validation - playerRegistration: Joi.object({ - email: Joi.string().email().max(320).required(), - username: Joi.string().alphanum().min(3).max(20).required(), - password: Joi.string().min(8).max(128).required(), - }), + // Player registration validation + playerRegistration: Joi.object({ + email: Joi.string().email().max(320).required(), + username: Joi.string().alphanum().min(3).max(20).required(), + password: Joi.string().min(8).max(128).required() + }), - // Player login validation - playerLogin: Joi.object({ - email: Joi.string().email().max(320).required(), - password: Joi.string().min(1).max(128).required(), - }), + // Player login validation + playerLogin: Joi.object({ + email: Joi.string().email().max(320).required(), + password: Joi.string().min(1).max(128).required() + }), - // Admin login validation - adminLogin: Joi.object({ - email: Joi.string().email().max(320).required(), - password: Joi.string().min(1).max(128).required(), - }), + // Admin login validation + adminLogin: Joi.object({ + email: Joi.string().email().max(320).required(), + password: Joi.string().min(1).max(128).required() + }), - // Colony creation validation - colonyCreation: Joi.object({ - name: Joi.string().min(3).max(50).required(), - coordinates: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required(), - planet_type_id: Joi.number().integer().min(1).required(), - }), + // Colony creation validation + colonyCreation: Joi.object({ + name: Joi.string().min(3).max(50).required(), + coordinates: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required(), + planet_type_id: Joi.number().integer().min(1).required() + }), - // Colony update validation - colonyUpdate: Joi.object({ - name: Joi.string().min(3).max(50).optional(), - }), + // Colony update validation + colonyUpdate: Joi.object({ + name: Joi.string().min(3).max(50).optional() + }), - // Fleet creation validation - fleetCreation: Joi.object({ - name: Joi.string().min(3).max(50).required(), - ships: Joi.array().items( - Joi.object({ - design_id: Joi.number().integer().min(1).required(), - quantity: Joi.number().integer().min(1).max(1000).required(), - }), - ).min(1).required(), - }), + // Fleet creation validation + fleetCreation: Joi.object({ + name: Joi.string().min(3).max(50).required(), + ships: Joi.array().items( + Joi.object({ + design_id: Joi.number().integer().min(1).required(), + quantity: Joi.number().integer().min(1).max(1000).required() + }) + ).min(1).required() + }), - // Fleet movement validation - fleetMovement: Joi.object({ - destination: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required(), - mission_type: Joi.string().valid('move', 'attack', 'colonize', 'transport').required(), - }), + // Fleet movement validation + fleetMovement: Joi.object({ + destination: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required(), + mission_type: Joi.string().valid('move', 'attack', 'colonize', 'transport').required() + }), - // Research initiation validation - researchInitiation: Joi.object({ - technology_id: Joi.number().integer().min(1).required(), - }), + // Research initiation validation + researchInitiation: Joi.object({ + technology_id: Joi.number().integer().min(1).required() + }), - // Message sending validation - messageSend: Joi.object({ - to_player_id: Joi.number().integer().min(1).required(), - subject: Joi.string().min(1).max(100).required(), - content: Joi.string().min(1).max(2000).required(), - }), + // Message sending validation + messageSend: Joi.object({ + to_player_id: Joi.number().integer().min(1).required(), + subject: Joi.string().min(1).max(100).required(), + content: Joi.string().min(1).max(2000).required() + }) }; /** * Pre-built validation middleware for common use cases */ const validators = { - // Parameter validators - validatePlayerId: validateRequest(commonSchemas.playerId, 'params'), - validatePagination: validateRequest(commonSchemas.pagination, 'query'), + // Parameter validators + validatePlayerId: validateRequest(commonSchemas.playerId, 'params'), + validatePagination: validateRequest(commonSchemas.pagination, 'query'), - // Authentication validators - validatePlayerRegistration: validateRequest(commonSchemas.playerRegistration, 'body'), - validatePlayerLogin: validateRequest(commonSchemas.playerLogin, 'body'), - validateAdminLogin: validateRequest(commonSchemas.adminLogin, 'body'), + // Authentication validators + validatePlayerRegistration: validateRequest(commonSchemas.playerRegistration, 'body'), + validatePlayerLogin: validateRequest(commonSchemas.playerLogin, 'body'), + validateAdminLogin: validateRequest(commonSchemas.adminLogin, 'body'), - // Game feature validators - validateColonyCreation: validateRequest(commonSchemas.colonyCreation, 'body'), - validateColonyUpdate: validateRequest(commonSchemas.colonyUpdate, 'body'), - validateFleetCreation: validateRequest(commonSchemas.fleetCreation, 'body'), - validateFleetMovement: validateRequest(commonSchemas.fleetMovement, 'body'), - validateResearchInitiation: validateRequest(commonSchemas.researchInitiation, 'body'), - validateMessageSend: validateRequest(commonSchemas.messageSend, 'body'), + // Game feature validators + validateColonyCreation: validateRequest(commonSchemas.colonyCreation, 'body'), + validateColonyUpdate: validateRequest(commonSchemas.colonyUpdate, 'body'), + validateFleetCreation: validateRequest(commonSchemas.fleetCreation, 'body'), + validateFleetMovement: validateRequest(commonSchemas.fleetMovement, 'body'), + validateResearchInitiation: validateRequest(commonSchemas.researchInitiation, 'body'), + validateMessageSend: validateRequest(commonSchemas.messageSend, 'body') }; /** * Custom validation helpers */ const validationHelpers = { - /** + /** * Create a custom validation schema for coordinates * @param {boolean} required - Whether the field is required * @returns {Joi.Schema} Joi schema for coordinates */ - coordinatesSchema(required = true) { - const schema = Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/); - return required ? schema.required() : schema.optional(); - }, + coordinatesSchema(required = true) { + let schema = Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/); + return required ? schema.required() : schema.optional(); + }, - /** + /** * Create a custom validation schema for player IDs * @param {boolean} required - Whether the field is required * @returns {Joi.Schema} Joi schema for player IDs */ - playerIdSchema(required = true) { - const schema = Joi.number().integer().min(1); - return required ? schema.required() : schema.optional(); - }, + playerIdSchema(required = true) { + let schema = Joi.number().integer().min(1); + return required ? schema.required() : schema.optional(); + }, - /** + /** * Create a custom validation schema for resource amounts * @param {number} min - Minimum value (default: 0) * @param {number} max - Maximum value (default: 999999999) * @returns {Joi.Schema} Joi schema for resource amounts */ - resourceAmountSchema(min = 0, max = 999999999) { - return Joi.number().integer().min(min).max(max); - }, + resourceAmountSchema(min = 0, max = 999999999) { + return Joi.number().integer().min(min).max(max); + }, - /** + /** * Create a validation schema for arrays with custom item validation * @param {Joi.Schema} itemSchema - Schema for array items * @param {number} minItems - Minimum number of items * @param {number} maxItems - Maximum number of items * @returns {Joi.Schema} Joi schema for arrays */ - arraySchema(itemSchema, minItems = 0, maxItems = 100) { - return Joi.array().items(itemSchema).min(minItems).max(maxItems); - }, + arraySchema(itemSchema, minItems = 0, maxItems = 100) { + return Joi.array().items(itemSchema).min(minItems).max(maxItems); + } }; /** @@ -269,42 +269,42 @@ const validationHelpers = { * @returns {Function} Express middleware function */ function sanitizeHTML(fields = []) { - return (req, res, next) => { - try { - if (!req.body || typeof req.body !== 'object') { - return next(); - } + return (req, res, next) => { + try { + if (!req.body || typeof req.body !== 'object') { + return next(); + } - const { sanitizeHTML: sanitize } = require('../utils/validation'); + const { sanitizeHTML: sanitize } = require('../utils/validation'); - fields.forEach(field => { - if (req.body[field] && typeof req.body[field] === 'string') { - req.body[field] = sanitize(req.body[field]); + fields.forEach(field => { + if (req.body[field] && typeof req.body[field] === 'string') { + req.body[field] = sanitize(req.body[field]); + } + }); + + next(); + + } catch (error) { + logger.error('HTML sanitization error', { + correlationId: req.correlationId, + error: error.message, + fields + }); + + return res.status(500).json({ + error: 'Internal server error', + message: 'Request processing failed', + correlationId: req.correlationId + }); } - }); - - next(); - - } catch (error) { - logger.error('HTML sanitization error', { - correlationId: req.correlationId, - error: error.message, - fields, - }); - - return res.status(500).json({ - error: 'Internal server error', - message: 'Request processing failed', - correlationId: req.correlationId, - }); - } - }; + }; } module.exports = { - validateRequest, - commonSchemas, - validators, - validationHelpers, - sanitizeHTML, -}; + validateRequest, + commonSchemas, + validators, + validationHelpers, + sanitizeHTML +}; \ No newline at end of file diff --git a/src/routes/admin.js b/src/routes/admin.js index 739f595..2c1de27 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -29,22 +29,21 @@ router.use(rateLimiters.admin); * Admin API Status and Information */ router.get('/', (req, res) => { - res.json({ - name: 'Shattered Void - Admin API', - version: process.env.npm_package_version || '0.1.0', - status: 'operational', - timestamp: new Date().toISOString(), - correlationId: req.correlationId, - endpoints: { - authentication: '/api/admin/auth', - players: '/api/admin/players', - system: '/api/admin/system', - events: '/api/admin/events', - analytics: '/api/admin/analytics', - combat: '/api/admin/combat', - }, - note: 'Administrative access required for all endpoints', - }); + res.json({ + name: 'Shattered Void - Admin API', + version: process.env.npm_package_version || '0.1.0', + status: 'operational', + timestamp: new Date().toISOString(), + correlationId: req.correlationId, + endpoints: { + authentication: '/api/admin/auth', + players: '/api/admin/players', + system: '/api/admin/system', + events: '/api/admin/events', + analytics: '/api/admin/analytics' + }, + note: 'Administrative access required for all endpoints' + }); }); /** @@ -55,50 +54,50 @@ const authRoutes = express.Router(); // Public admin authentication endpoints authRoutes.post('/login', - rateLimiters.auth, - validators.validateAdminLogin, - auditAdminAction('admin_login'), - adminAuthController.login, + rateLimiters.auth, + validators.validateAdminLogin, + auditAdminAction('admin_login'), + adminAuthController.login ); // Protected admin authentication endpoints authRoutes.post('/logout', - authenticateAdmin, - auditAdminAction('admin_logout'), - adminAuthController.logout, + authenticateAdmin, + auditAdminAction('admin_logout'), + adminAuthController.logout ); authRoutes.get('/me', - authenticateAdmin, - adminAuthController.getProfile, + authenticateAdmin, + adminAuthController.getProfile ); authRoutes.get('/verify', - authenticateAdmin, - adminAuthController.verifyToken, + authenticateAdmin, + adminAuthController.verifyToken ); authRoutes.post('/refresh', - rateLimiters.auth, - adminAuthController.refresh, + rateLimiters.auth, + adminAuthController.refresh ); authRoutes.get('/stats', - authenticateAdmin, - requirePermissions([ADMIN_PERMISSIONS.ANALYTICS_READ]), - auditAdminAction('view_system_stats'), - adminAuthController.getSystemStats, + authenticateAdmin, + requirePermissions([ADMIN_PERMISSIONS.ANALYTICS_READ]), + auditAdminAction('view_system_stats'), + adminAuthController.getSystemStats ); authRoutes.post('/change-password', - authenticateAdmin, - rateLimiters.auth, - validateRequest(require('joi').object({ - currentPassword: require('joi').string().required(), - newPassword: require('joi').string().min(8).max(128).required(), - }), 'body'), - auditAdminAction('admin_password_change'), - adminAuthController.changePassword, + authenticateAdmin, + rateLimiters.auth, + validateRequest(require('joi').object({ + currentPassword: require('joi').string().required(), + newPassword: require('joi').string().min(8).max(128).required() + }), 'body'), + auditAdminAction('admin_password_change'), + adminAuthController.changePassword ); // Mount admin authentication routes @@ -115,125 +114,125 @@ playerRoutes.use(authenticateAdmin); // Get players list playerRoutes.get('/', - requirePermissions([ADMIN_PERMISSIONS.PLAYER_DATA_READ]), - validators.validatePagination, - validateRequest(require('joi').object({ - search: require('joi').string().max(50).optional(), - activeOnly: require('joi').boolean().optional(), - sortBy: require('joi').string().valid('created_at', 'updated_at', 'username', 'email', 'last_login_at').default('created_at'), - sortOrder: require('joi').string().valid('asc', 'desc').default('desc'), - }), 'query'), - auditAdminAction('list_players'), - async (req, res) => { - try { - const { - page = 1, - limit = 20, - search = '', - activeOnly = null, - sortBy = 'created_at', - sortOrder = 'desc', - } = req.query; + requirePermissions([ADMIN_PERMISSIONS.PLAYER_DATA_READ]), + validators.validatePagination, + validateRequest(require('joi').object({ + search: require('joi').string().max(50).optional(), + activeOnly: require('joi').boolean().optional(), + sortBy: require('joi').string().valid('created_at', 'updated_at', 'username', 'email', 'last_login_at').default('created_at'), + sortOrder: require('joi').string().valid('asc', 'desc').default('desc') + }), 'query'), + auditAdminAction('list_players'), + async (req, res) => { + try { + const { + page = 1, + limit = 20, + search = '', + activeOnly = null, + sortBy = 'created_at', + sortOrder = 'desc' + } = req.query; - const result = await adminService.getPlayersList({ - page: parseInt(page), - limit: parseInt(limit), - search, - activeOnly, - sortBy, - sortOrder, - }, req.correlationId); + const result = await adminService.getPlayersList({ + page: parseInt(page), + limit: parseInt(limit), + search, + activeOnly, + sortBy, + sortOrder + }, req.correlationId); - res.json({ - success: true, - message: 'Players list retrieved successfully', - data: result, - correlationId: req.correlationId, - }); + res.json({ + success: true, + message: 'Players list retrieved successfully', + data: result, + correlationId: req.correlationId + }); - } catch (error) { - res.status(500).json({ - success: false, - error: 'Failed to retrieve players list', - message: error.message, - correlationId: req.correlationId, - }); + } catch (error) { + res.status(500).json({ + success: false, + error: 'Failed to retrieve players list', + message: error.message, + correlationId: req.correlationId + }); + } } - }, ); // Get specific player details playerRoutes.get('/:playerId', - requirePlayerAccess('playerId'), - validators.validatePlayerId, - auditAdminAction('view_player_details'), - async (req, res) => { - try { - const playerId = parseInt(req.params.playerId); - const playerDetails = await adminService.getPlayerDetails(playerId, req.correlationId); + requirePlayerAccess('playerId'), + validators.validatePlayerId, + auditAdminAction('view_player_details'), + async (req, res) => { + try { + const playerId = parseInt(req.params.playerId); + const playerDetails = await adminService.getPlayerDetails(playerId, req.correlationId); - res.json({ - success: true, - message: 'Player details retrieved successfully', - data: { - player: playerDetails, - }, - correlationId: req.correlationId, - }); + res.json({ + success: true, + message: 'Player details retrieved successfully', + data: { + player: playerDetails + }, + correlationId: req.correlationId + }); - } catch (error) { - const statusCode = error.name === 'NotFoundError' ? 404 : 500; - res.status(statusCode).json({ - success: false, - error: error.name === 'NotFoundError' ? 'Player not found' : 'Failed to retrieve player details', - message: error.message, - correlationId: req.correlationId, - }); + } catch (error) { + const statusCode = error.name === 'NotFoundError' ? 404 : 500; + res.status(statusCode).json({ + success: false, + error: error.name === 'NotFoundError' ? 'Player not found' : 'Failed to retrieve player details', + message: error.message, + correlationId: req.correlationId + }); + } } - }, ); // Update player status (activate/deactivate) playerRoutes.put('/:playerId/status', - requirePermissions([ADMIN_PERMISSIONS.PLAYER_MANAGEMENT]), - validators.validatePlayerId, - validateRequest(require('joi').object({ - isActive: require('joi').boolean().required(), - reason: require('joi').string().max(200).optional(), - }), 'body'), - auditAdminAction('update_player_status'), - async (req, res) => { - try { - const playerId = parseInt(req.params.playerId); - const { isActive, reason } = req.body; + requirePermissions([ADMIN_PERMISSIONS.PLAYER_MANAGEMENT]), + validators.validatePlayerId, + validateRequest(require('joi').object({ + isActive: require('joi').boolean().required(), + reason: require('joi').string().max(200).optional() + }), 'body'), + auditAdminAction('update_player_status'), + async (req, res) => { + try { + const playerId = parseInt(req.params.playerId); + const { isActive, reason } = req.body; - const updatedPlayer = await adminService.updatePlayerStatus( - playerId, - isActive, - req.correlationId, - ); + const updatedPlayer = await adminService.updatePlayerStatus( + playerId, + isActive, + req.correlationId + ); - res.json({ - success: true, - message: `Player ${isActive ? 'activated' : 'deactivated'} successfully`, - data: { - player: updatedPlayer, - action: isActive ? 'activated' : 'deactivated', - reason: reason || null, - }, - correlationId: req.correlationId, - }); + res.json({ + success: true, + message: `Player ${isActive ? 'activated' : 'deactivated'} successfully`, + data: { + player: updatedPlayer, + action: isActive ? 'activated' : 'deactivated', + reason: reason || null + }, + correlationId: req.correlationId + }); - } catch (error) { - const statusCode = error.name === 'NotFoundError' ? 404 : 500; - res.status(statusCode).json({ - success: false, - error: error.name === 'NotFoundError' ? 'Player not found' : 'Failed to update player status', - message: error.message, - correlationId: req.correlationId, - }); + } catch (error) { + const statusCode = error.name === 'NotFoundError' ? 404 : 500; + res.status(statusCode).json({ + success: false, + error: error.name === 'NotFoundError' ? 'Player not found' : 'Failed to update player status', + message: error.message, + correlationId: req.correlationId + }); + } } - }, ); // Mount player management routes @@ -250,124 +249,118 @@ systemRoutes.use(authenticateAdmin); // Get detailed system statistics systemRoutes.get('/stats', - requirePermissions([ADMIN_PERMISSIONS.SYSTEM_MANAGEMENT]), - auditAdminAction('view_detailed_system_stats'), - async (req, res) => { - try { - const stats = await adminService.getSystemStats(req.correlationId); + requirePermissions([ADMIN_PERMISSIONS.SYSTEM_MANAGEMENT]), + auditAdminAction('view_detailed_system_stats'), + async (req, res) => { + try { + const stats = await adminService.getSystemStats(req.correlationId); - // Add additional system information - const systemInfo = { - ...stats, - server: { - version: process.env.npm_package_version || '0.1.0', - environment: process.env.NODE_ENV || 'development', - uptime: process.uptime(), - nodeVersion: process.version, - memory: { - used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), - total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), - rss: Math.round(process.memoryUsage().rss / 1024 / 1024), - }, - }, - }; + // Add additional system information + const systemInfo = { + ...stats, + server: { + version: process.env.npm_package_version || '0.1.0', + environment: process.env.NODE_ENV || 'development', + uptime: process.uptime(), + nodeVersion: process.version, + memory: { + used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), + total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), + rss: Math.round(process.memoryUsage().rss / 1024 / 1024) + } + } + }; - res.json({ - success: true, - message: 'System statistics retrieved successfully', - data: systemInfo, - correlationId: req.correlationId, - }); + res.json({ + success: true, + message: 'System statistics retrieved successfully', + data: systemInfo, + correlationId: req.correlationId + }); - } catch (error) { - res.status(500).json({ - success: false, - error: 'Failed to retrieve system statistics', - message: error.message, - correlationId: req.correlationId, - }); + } catch (error) { + res.status(500).json({ + success: false, + error: 'Failed to retrieve system statistics', + message: error.message, + correlationId: req.correlationId + }); + } } - }, ); // System health check systemRoutes.get('/health', - requirePermissions([ADMIN_PERMISSIONS.SYSTEM_MANAGEMENT]), - async (req, res) => { - try { - // TODO: Implement comprehensive health checks - // - Database connectivity - // - Redis connectivity - // - WebSocket server status - // - External service connectivity + requirePermissions([ADMIN_PERMISSIONS.SYSTEM_MANAGEMENT]), + async (req, res) => { + try { + // TODO: Implement comprehensive health checks + // - Database connectivity + // - Redis connectivity + // - WebSocket server status + // - External service connectivity - const healthStatus = { - status: 'healthy', - timestamp: new Date().toISOString(), - services: { - database: 'healthy', - redis: 'healthy', - websocket: 'healthy', - }, - performance: { - uptime: process.uptime(), - memory: process.memoryUsage(), - cpu: process.cpuUsage(), - }, - }; + const healthStatus = { + status: 'healthy', + timestamp: new Date().toISOString(), + services: { + database: 'healthy', + redis: 'healthy', + websocket: 'healthy' + }, + performance: { + uptime: process.uptime(), + memory: process.memoryUsage(), + cpu: process.cpuUsage() + } + }; - res.json({ - success: true, - message: 'System health check completed', - data: healthStatus, - correlationId: req.correlationId, - }); + res.json({ + success: true, + message: 'System health check completed', + data: healthStatus, + correlationId: req.correlationId + }); - } catch (error) { - res.status(500).json({ - success: false, - error: 'Health check failed', - message: error.message, - correlationId: req.correlationId, - }); + } catch (error) { + res.status(500).json({ + success: false, + error: 'Health check failed', + message: error.message, + correlationId: req.correlationId + }); + } } - }, ); // Mount system routes router.use('/system', systemRoutes); -/** - * Combat Management Routes - * /api/admin/combat/* - */ -router.use('/combat', require('./admin/combat')); - /** * Events Management Routes (placeholder) * /api/admin/events/* */ router.get('/events', - authenticateAdmin, - requirePermissions([ADMIN_PERMISSIONS.EVENT_MANAGEMENT]), - validators.validatePagination, - auditAdminAction('view_events'), - (req, res) => { - res.json({ - success: true, - message: 'Events endpoint - feature not yet implemented', - data: { - events: [], - pagination: { - page: 1, - limit: 20, - total: 0, - totalPages: 0, - }, - }, - correlationId: req.correlationId, - }); - }, + authenticateAdmin, + requirePermissions([ADMIN_PERMISSIONS.EVENT_MANAGEMENT]), + validators.validatePagination, + auditAdminAction('view_events'), + (req, res) => { + res.json({ + success: true, + message: 'Events endpoint - feature not yet implemented', + data: { + events: [], + pagination: { + page: 1, + limit: 20, + total: 0, + totalPages: 0 + } + }, + correlationId: req.correlationId + }); + } ); /** @@ -375,34 +368,34 @@ router.get('/events', * /api/admin/analytics/* */ router.get('/analytics', - authenticateAdmin, - requirePermissions([ADMIN_PERMISSIONS.ANALYTICS_READ]), - auditAdminAction('view_analytics'), - (req, res) => { - res.json({ - success: true, - message: 'Analytics endpoint - feature not yet implemented', - data: { - analytics: {}, - timeRange: 'daily', - metrics: [], - }, - correlationId: req.correlationId, - }); - }, + authenticateAdmin, + requirePermissions([ADMIN_PERMISSIONS.ANALYTICS_READ]), + auditAdminAction('view_analytics'), + (req, res) => { + res.json({ + success: true, + message: 'Analytics endpoint - feature not yet implemented', + data: { + analytics: {}, + timeRange: 'daily', + metrics: [] + }, + correlationId: req.correlationId + }); + } ); /** * Error handling for admin routes */ router.use('*', (req, res) => { - res.status(404).json({ - success: false, - error: 'Admin API endpoint not found', - message: `The endpoint ${req.method} ${req.originalUrl} does not exist`, - correlationId: req.correlationId, - timestamp: new Date().toISOString(), - }); + res.status(404).json({ + success: false, + error: 'Admin API endpoint not found', + message: `The endpoint ${req.method} ${req.originalUrl} does not exist`, + correlationId: req.correlationId, + timestamp: new Date().toISOString() + }); }); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/src/routes/admin/combat.js b/src/routes/admin/combat.js deleted file mode 100644 index 98a5b3b..0000000 --- a/src/routes/admin/combat.js +++ /dev/null @@ -1,345 +0,0 @@ -/** - * Admin Combat Routes - * Administrative endpoints for combat system management - */ - -const express = require('express'); -const router = express.Router(); - -// Import controllers -const { - getCombatStatistics, - getCombatQueue, - forceResolveCombat, - cancelBattle, - getCombatConfigurations, - saveCombatConfiguration, - deleteCombatConfiguration, -} = require('../../controllers/admin/combat.controller'); - -// Import middleware -const { authenticateAdmin } = require('../../middleware/admin.middleware'); -const { - validateCombatQueueQuery, - validateParams, - logCombatAction, -} = require('../../middleware/combat.middleware'); -const { validateCombatConfiguration } = require('../../validators/combat.validators'); - -// Apply admin authentication to all routes -router.use(authenticateAdmin); - -/** - * @route GET /api/admin/combat/statistics - * @desc Get comprehensive combat system statistics - * @access Admin - */ -router.get('/statistics', - logCombatAction('admin_get_combat_statistics'), - getCombatStatistics, -); - -/** - * @route GET /api/admin/combat/queue - * @desc Get combat queue with filtering options - * @access Admin - */ -router.get('/queue', - logCombatAction('admin_get_combat_queue'), - validateCombatQueueQuery, - getCombatQueue, -); - -/** - * @route POST /api/admin/combat/resolve/:battleId - * @desc Force resolve a specific battle - * @access Admin - */ -router.post('/resolve/:battleId', - logCombatAction('admin_force_resolve_combat'), - validateParams('battleId'), - forceResolveCombat, -); - -/** - * @route POST /api/admin/combat/cancel/:battleId - * @desc Cancel a battle - * @access Admin - */ -router.post('/cancel/:battleId', - logCombatAction('admin_cancel_battle'), - validateParams('battleId'), - (req, res, next) => { - // Validate cancel reason in request body - const { reason } = req.body; - if (!reason || typeof reason !== 'string' || reason.trim().length < 5) { - return res.status(400).json({ - error: 'Cancel reason is required and must be at least 5 characters', - code: 'INVALID_CANCEL_REASON', - }); - } - next(); - }, - cancelBattle, -); - -/** - * @route GET /api/admin/combat/configurations - * @desc Get all combat configurations - * @access Admin - */ -router.get('/configurations', - logCombatAction('admin_get_combat_configurations'), - getCombatConfigurations, -); - -/** - * @route POST /api/admin/combat/configurations - * @desc Create new combat configuration - * @access Admin - */ -router.post('/configurations', - logCombatAction('admin_create_combat_configuration'), - (req, res, next) => { - const { error, value } = validateCombatConfiguration(req.body); - if (error) { - const details = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - })); - - return res.status(400).json({ - error: 'Validation failed', - code: 'VALIDATION_ERROR', - details, - }); - } - req.body = value; - next(); - }, - saveCombatConfiguration, -); - -/** - * @route PUT /api/admin/combat/configurations/:configId - * @desc Update existing combat configuration - * @access Admin - */ -router.put('/configurations/:configId', - logCombatAction('admin_update_combat_configuration'), - validateParams('configId'), - (req, res, next) => { - const { error, value } = validateCombatConfiguration(req.body); - if (error) { - const details = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - })); - - return res.status(400).json({ - error: 'Validation failed', - code: 'VALIDATION_ERROR', - details, - }); - } - req.body = value; - next(); - }, - saveCombatConfiguration, -); - -/** - * @route DELETE /api/admin/combat/configurations/:configId - * @desc Delete combat configuration - * @access Admin - */ -router.delete('/configurations/:configId', - logCombatAction('admin_delete_combat_configuration'), - validateParams('configId'), - deleteCombatConfiguration, -); - -/** - * @route GET /api/admin/combat/battles - * @desc Get all battles with filtering and pagination - * @access Admin - */ -router.get('/battles', - logCombatAction('admin_get_battles'), - async (req, res, next) => { - try { - const { - status, - battle_type, - location, - limit = 50, - offset = 0, - start_date, - end_date, - } = req.query; - - const db = require('../../database/connection'); - const logger = require('../../utils/logger'); - - let query = db('battles') - .select([ - 'battles.*', - 'combat_configurations.config_name', - 'combat_configurations.combat_type', - ]) - .leftJoin('combat_configurations', 'battles.combat_configuration_id', 'combat_configurations.id') - .orderBy('battles.started_at', 'desc') - .limit(parseInt(limit)) - .offset(parseInt(offset)); - - if (status) { - query = query.where('battles.status', status); - } - - if (battle_type) { - query = query.where('battles.battle_type', battle_type); - } - - if (location) { - query = query.where('battles.location', location); - } - - if (start_date) { - query = query.where('battles.started_at', '>=', new Date(start_date)); - } - - if (end_date) { - query = query.where('battles.started_at', '<=', new Date(end_date)); - } - - const battles = await query; - - // Get total count for pagination - let countQuery = db('battles').count('* as total'); - - if (status) countQuery = countQuery.where('status', status); - if (battle_type) countQuery = countQuery.where('battle_type', battle_type); - if (location) countQuery = countQuery.where('location', location); - if (start_date) countQuery = countQuery.where('started_at', '>=', new Date(start_date)); - if (end_date) countQuery = countQuery.where('started_at', '<=', new Date(end_date)); - - const [{ total }] = await countQuery; - - // Parse participants JSON for each battle - const battlesWithParsedParticipants = battles.map(battle => ({ - ...battle, - participants: JSON.parse(battle.participants), - battle_data: battle.battle_data ? JSON.parse(battle.battle_data) : null, - result: battle.result ? JSON.parse(battle.result) : null, - })); - - logger.info('Admin battles retrieved', { - correlationId: req.correlationId, - adminUser: req.user.id, - count: battles.length, - total: parseInt(total), - }); - - res.json({ - success: true, - data: { - battles: battlesWithParsedParticipants, - pagination: { - total: parseInt(total), - limit: parseInt(limit), - offset: parseInt(offset), - hasMore: (parseInt(offset) + parseInt(limit)) < parseInt(total), - }, - }, - }); - - } catch (error) { - next(error); - } - }, -); - -/** - * @route GET /api/admin/combat/encounters/:encounterId - * @desc Get detailed combat encounter for admin review - * @access Admin - */ -router.get('/encounters/:encounterId', - logCombatAction('admin_get_combat_encounter'), - validateParams('encounterId'), - async (req, res, next) => { - try { - const encounterId = parseInt(req.params.encounterId); - const db = require('../../database/connection'); - const logger = require('../../utils/logger'); - - // Get encounter with all related data - const encounter = await db('combat_encounters') - .select([ - 'combat_encounters.*', - 'battles.battle_type', - 'battles.participants', - 'battles.started_at as battle_started', - 'battles.completed_at as battle_completed', - 'attacker_fleet.name as attacker_fleet_name', - 'attacker_player.username as attacker_username', - 'defender_fleet.name as defender_fleet_name', - 'defender_player.username as defender_username', - 'defender_colony.name as defender_colony_name', - 'colony_player.username as colony_owner_username', - ]) - .join('battles', 'combat_encounters.battle_id', 'battles.id') - .leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id') - .leftJoin('players as attacker_player', 'attacker_fleet.player_id', 'attacker_player.id') - .leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id') - .leftJoin('players as defender_player', 'defender_fleet.player_id', 'defender_player.id') - .leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id') - .leftJoin('players as colony_player', 'defender_colony.player_id', 'colony_player.id') - .where('combat_encounters.id', encounterId) - .first(); - - if (!encounter) { - return res.status(404).json({ - error: 'Combat encounter not found', - code: 'ENCOUNTER_NOT_FOUND', - }); - } - - // Get combat logs - const combatLogs = await db('combat_logs') - .where('encounter_id', encounterId) - .orderBy('round_number') - .orderBy('timestamp'); - - const detailedEncounter = { - ...encounter, - participants: JSON.parse(encounter.participants), - initial_forces: JSON.parse(encounter.initial_forces), - final_forces: JSON.parse(encounter.final_forces), - casualties: JSON.parse(encounter.casualties), - combat_log: JSON.parse(encounter.combat_log), - loot_awarded: JSON.parse(encounter.loot_awarded), - detailed_logs: combatLogs.map(log => ({ - ...log, - event_data: JSON.parse(log.event_data), - })), - }; - - logger.info('Admin combat encounter retrieved', { - correlationId: req.correlationId, - adminUser: req.user.id, - encounterId, - }); - - res.json({ - success: true, - data: detailedEncounter, - }); - - } catch (error) { - next(error); - } - }, -); - -module.exports = router; diff --git a/src/routes/admin/index.js b/src/routes/admin/index.js index e5dada4..e2fc6ac 100644 --- a/src/routes/admin/index.js +++ b/src/routes/admin/index.js @@ -39,4 +39,4 @@ router.get('/status', authenticateToken('admin'), asyncHandler(async (req, res) }); })); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/src/routes/admin/system.js b/src/routes/admin/system.js index 138d2c0..e69de29 100644 --- a/src/routes/admin/system.js +++ b/src/routes/admin/system.js @@ -1,586 +0,0 @@ -/** - * Admin System Management Routes - * Provides administrative controls for game tick system, configuration, and monitoring - */ - -const express = require('express'); -const router = express.Router(); -const logger = require('../../utils/logger'); -const { - gameTickService, - getGameTickStatus, - triggerManualTick, -} = require('../../services/game-tick.service'); -const db = require('../../database/connection'); -const { v4: uuidv4 } = require('uuid'); - -/** - * Get game tick system status and metrics - * GET /admin/system/tick/status - */ -router.get('/tick/status', async (req, res) => { - const correlationId = req.correlationId || uuidv4(); - - try { - logger.info('Admin requesting game tick status', { - correlationId, - adminId: req.user?.id, - adminUsername: req.user?.username, - }); - - const status = getGameTickStatus(); - - // Get recent tick logs - const recentLogs = await db('game_tick_log') - .select('*') - .orderBy('tick_number', 'desc') - .limit(10); - - // Get performance statistics - const performanceStats = await db('game_tick_log') - .select( - db.raw('AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) * 1000) as avg_duration_ms'), - db.raw('COUNT(*) as total_ticks'), - db.raw('COUNT(*) FILTER (WHERE status = \'completed\') as successful_ticks'), - db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failed_ticks'), - db.raw('MAX(tick_number) as latest_tick'), - ) - .where('started_at', '>=', db.raw('NOW() - INTERVAL \'24 hours\'')) - .first(); - - // Get user group statistics - const userGroupStats = await db('game_tick_log') - .select( - 'user_group', - db.raw('COUNT(*) as tick_count'), - db.raw('AVG(processed_players) as avg_players'), - db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failures'), - ) - .where('started_at', '>=', db.raw('NOW() - INTERVAL \'24 hours\'')) - .groupBy('user_group') - .orderBy('user_group'); - - res.json({ - success: true, - data: { - service: status, - performance: performanceStats, - userGroups: userGroupStats, - recentLogs: recentLogs.map(log => ({ - id: log.id, - tickNumber: log.tick_number, - userGroup: log.user_group, - status: log.status, - processedPlayers: log.processed_players, - duration: log.performance_metrics?.duration_ms, - startedAt: log.started_at, - completedAt: log.completed_at, - errorMessage: log.error_message, - })), - }, - timestamp: new Date().toISOString(), - correlationId, - }); - - } catch (error) { - logger.error('Failed to get game tick status', { - correlationId, - adminId: req.user?.id, - error: error.message, - stack: error.stack, - }); - - res.status(500).json({ - success: false, - error: 'Failed to retrieve game tick status', - correlationId, - }); - } -}); - -/** - * Trigger manual game tick - * POST /admin/system/tick/trigger - */ -router.post('/tick/trigger', async (req, res) => { - const correlationId = req.correlationId || uuidv4(); - - try { - logger.info('Admin triggering manual game tick', { - correlationId, - adminId: req.user?.id, - adminUsername: req.user?.username, - }); - - const result = await triggerManualTick(correlationId); - - // Log admin action - await db('audit_log').insert({ - entity_type: 'game_tick', - entity_id: 0, - action: 'manual_tick_triggered', - actor_type: 'admin', - actor_id: req.user?.id, - changes: { - correlation_id: correlationId, - triggered_by: req.user?.username, - }, - ip_address: req.ip, - user_agent: req.get('User-Agent'), - }); - - res.json({ - success: true, - message: 'Manual game tick triggered successfully', - data: result, - timestamp: new Date().toISOString(), - correlationId, - }); - - } catch (error) { - logger.error('Failed to trigger manual game tick', { - correlationId, - adminId: req.user?.id, - error: error.message, - stack: error.stack, - }); - - res.status(500).json({ - success: false, - error: error.message || 'Failed to trigger manual game tick', - correlationId, - }); - } -}); - -/** - * Update game tick configuration - * PUT /admin/system/tick/config - */ -router.put('/tick/config', async (req, res) => { - const correlationId = req.correlationId || uuidv4(); - - try { - const { - tick_interval_ms, - user_groups_count, - max_retry_attempts, - bonus_tick_threshold, - retry_delay_ms, - } = req.body; - - logger.info('Admin updating game tick configuration', { - correlationId, - adminId: req.user?.id, - adminUsername: req.user?.username, - newConfig: req.body, - }); - - // Validate configuration values - const validationErrors = []; - - if (tick_interval_ms && (tick_interval_ms < 10000 || tick_interval_ms > 3600000)) { - validationErrors.push('tick_interval_ms must be between 10000 and 3600000 (10 seconds to 1 hour)'); - } - - if (user_groups_count && (user_groups_count < 1 || user_groups_count > 50)) { - validationErrors.push('user_groups_count must be between 1 and 50'); - } - - if (max_retry_attempts && (max_retry_attempts < 1 || max_retry_attempts > 10)) { - validationErrors.push('max_retry_attempts must be between 1 and 10'); - } - - if (validationErrors.length > 0) { - return res.status(400).json({ - success: false, - error: 'Configuration validation failed', - details: validationErrors, - correlationId, - }); - } - - // Get current configuration - const currentConfig = await db('game_tick_config') - .where('is_active', true) - .first(); - - if (!currentConfig) { - return res.status(404).json({ - success: false, - error: 'No active game tick configuration found', - correlationId, - }); - } - - // Update configuration - const updatedConfig = await db('game_tick_config') - .where('id', currentConfig.id) - .update({ - tick_interval_ms: tick_interval_ms || currentConfig.tick_interval_ms, - user_groups_count: user_groups_count || currentConfig.user_groups_count, - max_retry_attempts: max_retry_attempts || currentConfig.max_retry_attempts, - bonus_tick_threshold: bonus_tick_threshold || currentConfig.bonus_tick_threshold, - retry_delay_ms: retry_delay_ms || currentConfig.retry_delay_ms, - updated_at: new Date(), - }) - .returning('*'); - - // Log admin action - await db('audit_log').insert({ - entity_type: 'game_tick_config', - entity_id: currentConfig.id, - action: 'configuration_updated', - actor_type: 'admin', - actor_id: req.user?.id, - changes: { - before: currentConfig, - after: updatedConfig[0], - updated_by: req.user?.username, - }, - ip_address: req.ip, - user_agent: req.get('User-Agent'), - }); - - // Reload configuration in the service - await gameTickService.loadConfig(); - - res.json({ - success: true, - message: 'Game tick configuration updated successfully', - data: { - previousConfig: currentConfig, - newConfig: updatedConfig[0], - }, - timestamp: new Date().toISOString(), - correlationId, - }); - - } catch (error) { - logger.error('Failed to update game tick configuration', { - correlationId, - adminId: req.user?.id, - error: error.message, - stack: error.stack, - }); - - res.status(500).json({ - success: false, - error: 'Failed to update game tick configuration', - correlationId, - }); - } -}); - -/** - * Get game tick logs with filtering - * GET /admin/system/tick/logs - */ -router.get('/tick/logs', async (req, res) => { - const correlationId = req.correlationId || uuidv4(); - - try { - const { - page = 1, - limit = 50, - status, - userGroup, - tickNumber, - startDate, - endDate, - } = req.query; - - const pageNum = parseInt(page); - const limitNum = Math.min(parseInt(limit), 100); // Max 100 records per page - const offset = (pageNum - 1) * limitNum; - - let query = db('game_tick_log').select('*'); - - // Apply filters - if (status) { - query = query.where('status', status); - } - - if (userGroup !== undefined) { - query = query.where('user_group', parseInt(userGroup)); - } - - if (tickNumber) { - query = query.where('tick_number', parseInt(tickNumber)); - } - - if (startDate) { - query = query.where('started_at', '>=', new Date(startDate)); - } - - if (endDate) { - query = query.where('started_at', '<=', new Date(endDate)); - } - - // Get total count for pagination - const countQuery = query.clone().clearSelect().count('* as total'); - const [{ total }] = await countQuery; - - // Get paginated results - const logs = await query - .orderBy('tick_number', 'desc') - .orderBy('user_group', 'asc') - .limit(limitNum) - .offset(offset); - - res.json({ - success: true, - data: { - logs: logs.map(log => ({ - id: log.id, - tickNumber: log.tick_number, - userGroup: log.user_group, - status: log.status, - processedPlayers: log.processed_players, - retryCount: log.retry_count, - errorMessage: log.error_message, - performanceMetrics: log.performance_metrics, - startedAt: log.started_at, - completedAt: log.completed_at, - })), - pagination: { - page: pageNum, - limit: limitNum, - total: parseInt(total), - pages: Math.ceil(total / limitNum), - }, - }, - timestamp: new Date().toISOString(), - correlationId, - }); - - } catch (error) { - logger.error('Failed to get game tick logs', { - correlationId, - adminId: req.user?.id, - error: error.message, - stack: error.stack, - }); - - res.status(500).json({ - success: false, - error: 'Failed to retrieve game tick logs', - correlationId, - }); - } -}); - -/** - * Get system performance metrics - * GET /admin/system/performance - */ -router.get('/performance', async (req, res) => { - const correlationId = req.correlationId || uuidv4(); - - try { - const { timeRange = '24h' } = req.query; - - let interval; - switch (timeRange) { - case '1h': - interval = '1 hour'; - break; - case '24h': - interval = '24 hours'; - break; - case '7d': - interval = '7 days'; - break; - case '30d': - interval = '30 days'; - break; - default: - interval = '24 hours'; - } - - // Get tick performance metrics - const tickMetrics = await db('game_tick_log') - .select( - db.raw('DATE_TRUNC(\'hour\', started_at) as hour'), - db.raw('COUNT(*) as total_ticks'), - db.raw('COUNT(*) FILTER (WHERE status = \'completed\') as successful_ticks'), - db.raw('COUNT(*) FILTER (WHERE status = \'failed\') as failed_ticks'), - db.raw('AVG(processed_players) as avg_players_processed'), - db.raw('AVG(EXTRACT(EPOCH FROM (completed_at - started_at)) * 1000) as avg_duration_ms'), - ) - .where('started_at', '>=', db.raw(`NOW() - INTERVAL '${interval}'`)) - .groupBy(db.raw('DATE_TRUNC(\'hour\', started_at)')) - .orderBy('hour'); - - // Get database performance metrics - const dbMetrics = await db.raw(` - SELECT - schemaname, - tablename, - n_tup_ins as inserts, - n_tup_upd as updates, - n_tup_del as deletes, - seq_scan as sequential_scans, - idx_scan as index_scans - FROM pg_stat_user_tables - WHERE schemaname = 'public' - ORDER BY (n_tup_ins + n_tup_upd + n_tup_del) DESC - LIMIT 10 - `); - - // Get active player count - const playerStats = await db('players') - .select( - db.raw('COUNT(*) FILTER (WHERE is_active = true) as active_players'), - db.raw('COUNT(*) FILTER (WHERE last_login >= NOW() - INTERVAL \'24 hours\') as recent_players'), - db.raw('COUNT(*) as total_players'), - ) - .first(); - - res.json({ - success: true, - data: { - timeRange, - tickMetrics: tickMetrics.map(metric => ({ - hour: metric.hour, - totalTicks: parseInt(metric.total_ticks), - successfulTicks: parseInt(metric.successful_ticks), - failedTicks: parseInt(metric.failed_ticks), - successRate: metric.total_ticks > 0 ? - ((metric.successful_ticks / metric.total_ticks) * 100).toFixed(2) : 0, - avgPlayersProcessed: parseFloat(metric.avg_players_processed || 0).toFixed(1), - avgDurationMs: parseFloat(metric.avg_duration_ms || 0).toFixed(2), - })), - databaseMetrics: dbMetrics.rows, - playerStats, - }, - timestamp: new Date().toISOString(), - correlationId, - }); - - } catch (error) { - logger.error('Failed to get system performance metrics', { - correlationId, - adminId: req.user?.id, - error: error.message, - stack: error.stack, - }); - - res.status(500).json({ - success: false, - error: 'Failed to retrieve performance metrics', - correlationId, - }); - } -}); - -/** - * Stop game tick service - * POST /admin/system/tick/stop - */ -router.post('/tick/stop', async (req, res) => { - const correlationId = req.correlationId || uuidv4(); - - try { - logger.warn('Admin stopping game tick service', { - correlationId, - adminId: req.user?.id, - adminUsername: req.user?.username, - }); - - gameTickService.stop(); - - // Log admin action - await db('audit_log').insert({ - entity_type: 'game_tick', - entity_id: 0, - action: 'service_stopped', - actor_type: 'admin', - actor_id: req.user?.id, - changes: { - correlation_id: correlationId, - stopped_by: req.user?.username, - timestamp: new Date().toISOString(), - }, - ip_address: req.ip, - user_agent: req.get('User-Agent'), - }); - - res.json({ - success: true, - message: 'Game tick service stopped successfully', - timestamp: new Date().toISOString(), - correlationId, - }); - - } catch (error) { - logger.error('Failed to stop game tick service', { - correlationId, - adminId: req.user?.id, - error: error.message, - }); - - res.status(500).json({ - success: false, - error: 'Failed to stop game tick service', - correlationId, - }); - } -}); - -/** - * Start game tick service - * POST /admin/system/tick/start - */ -router.post('/tick/start', async (req, res) => { - const correlationId = req.correlationId || uuidv4(); - - try { - logger.info('Admin starting game tick service', { - correlationId, - adminId: req.user?.id, - adminUsername: req.user?.username, - }); - - await gameTickService.initialize(); - - // Log admin action - await db('audit_log').insert({ - entity_type: 'game_tick', - entity_id: 0, - action: 'service_started', - actor_type: 'admin', - actor_id: req.user?.id, - changes: { - correlation_id: correlationId, - started_by: req.user?.username, - timestamp: new Date().toISOString(), - }, - ip_address: req.ip, - user_agent: req.get('User-Agent'), - }); - - res.json({ - success: true, - message: 'Game tick service started successfully', - data: gameTickService.getStatus(), - timestamp: new Date().toISOString(), - correlationId, - }); - - } catch (error) { - logger.error('Failed to start game tick service', { - correlationId, - adminId: req.user?.id, - error: error.message, - }); - - res.status(500).json({ - success: false, - error: error.message || 'Failed to start game tick service', - correlationId, - }); - } -}); - -module.exports = router; diff --git a/src/routes/api.js b/src/routes/api.js index 1257e8b..e04937c 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -8,33 +8,10 @@ const router = express.Router(); // Import middleware const { authenticatePlayer, optionalPlayerAuth, requireOwnership, injectPlayerId } = require('../middleware/auth.middleware'); -const { authenticateToken } = require('../middleware/auth'); // Standardized auth const { rateLimiters } = require('../middleware/rateLimit.middleware'); const { validators, validateRequest } = require('../middleware/validation.middleware'); -const { - accountLockoutProtection, - rateLimiter, - passwordStrengthValidator, - requireEmailVerification, - sanitizeInput -} = require('../middleware/security.middleware'); -const { - validateRequest: validateAuthRequest, - validateRegistrationUniqueness, - registerPlayerSchema, - loginPlayerSchema, - verifyEmailSchema, - resendVerificationSchema, - requestPasswordResetSchema, - resetPasswordSchema, - changePasswordSchema -} = require('../validators/auth.validators'); const corsMiddleware = require('../middleware/cors.middleware'); -// Use standardized authentication for players -const authenticatePlayerToken = authenticateToken('player'); -const optionalPlayerToken = require('../middleware/auth').optionalAuth('player'); - // Import controllers const authController = require('../controllers/api/auth.controller'); const playerController = require('../controllers/api/player.controller'); @@ -62,8 +39,7 @@ router.get('/', (req, res) => { colonies: '/api/colonies', fleets: '/api/fleets', research: '/api/research', - galaxy: '/api/galaxy', - combat: '/api/combat' + galaxy: '/api/galaxy' } } }); @@ -77,24 +53,20 @@ const authRoutes = express.Router(); // Public authentication endpoints (with stricter rate limiting) authRoutes.post('/register', - rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'registration' }), - sanitizeInput(['email', 'username']), - validateAuthRequest(registerPlayerSchema), - validateRegistrationUniqueness(), + rateLimiters.auth, + validators.validatePlayerRegistration, authController.register ); authRoutes.post('/login', - rateLimiter({ maxRequests: 5, windowMinutes: 15, action: 'login' }), - accountLockoutProtection, - sanitizeInput(['email']), - validateAuthRequest(loginPlayerSchema), + rateLimiters.auth, + validators.validatePlayerLogin, authController.login ); // Protected authentication endpoints authRoutes.post('/logout', - authenticatePlayerToken, + authenticatePlayer, authController.logout ); @@ -104,84 +76,33 @@ authRoutes.post('/refresh', ); authRoutes.get('/me', - authenticatePlayerToken, + authenticatePlayer, authController.getProfile ); authRoutes.put('/me', - authenticatePlayerToken, - requireEmailVerification, - rateLimiter({ maxRequests: 5, windowMinutes: 60, action: 'profile_update' }), - sanitizeInput(['username', 'displayName', 'bio']), + authenticatePlayer, validateRequest(require('joi').object({ - username: require('joi').string().alphanum().min(3).max(20).optional(), - displayName: require('joi').string().min(1).max(50).optional(), - bio: require('joi').string().max(500).optional() + username: require('joi').string().alphanum().min(3).max(20).optional() }), 'body'), authController.updateProfile ); authRoutes.get('/verify', - authenticatePlayerToken, + authenticatePlayer, authController.verifyToken ); authRoutes.post('/change-password', - authenticatePlayerToken, - rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'password_change' }), - validateAuthRequest(changePasswordSchema), - passwordStrengthValidator('newPassword'), + authenticatePlayer, + rateLimiters.auth, + validateRequest(require('joi').object({ + currentPassword: require('joi').string().required(), + newPassword: require('joi').string().min(8).max(128).required() + }), 'body'), authController.changePassword ); -// Email verification endpoints -authRoutes.post('/verify-email', - rateLimiter({ maxRequests: 5, windowMinutes: 15, action: 'email_verification' }), - validateAuthRequest(verifyEmailSchema), - authController.verifyEmail -); - -authRoutes.post('/resend-verification', - rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'resend_verification' }), - sanitizeInput(['email']), - validateAuthRequest(resendVerificationSchema), - authController.resendVerification -); - -// Password reset endpoints -authRoutes.post('/request-password-reset', - rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'password_reset_request' }), - sanitizeInput(['email']), - validateAuthRequest(requestPasswordResetSchema), - authController.requestPasswordReset -); - -authRoutes.post('/reset-password', - rateLimiter({ maxRequests: 3, windowMinutes: 60, action: 'password_reset' }), - validateAuthRequest(resetPasswordSchema), - passwordStrengthValidator('newPassword'), - authController.resetPassword -); - -// Security utility endpoints -authRoutes.post('/check-password-strength', - rateLimiter({ maxRequests: 10, windowMinutes: 5, action: 'password_check' }), - authController.checkPasswordStrength -); - -authRoutes.get('/security-status', - authenticatePlayerToken, - authController.getSecurityStatus -); - -// Development and diagnostic endpoints (only available in development) -if (process.env.NODE_ENV === 'development') { - authRoutes.get('/debug/registration-test', - rateLimiter({ maxRequests: 10, windowMinutes: 5, action: 'diagnostic' }), - authController.registrationDiagnostic - ); -} - // Mount authentication routes router.use('/auth', authRoutes); @@ -189,18 +110,18 @@ router.use('/auth', authRoutes); * Player Management Routes * /api/player/* */ -const playerManagementRoutes = express.Router(); +const playerRoutes = express.Router(); // All player routes require authentication -playerManagementRoutes.use(authenticatePlayerToken); +playerRoutes.use(authenticatePlayer); -playerManagementRoutes.get('/dashboard', playerController.getDashboard); +playerRoutes.get('/dashboard', playerController.getDashboard); -playerManagementRoutes.get('/resources', playerController.getResources); +playerRoutes.get('/resources', playerController.getResources); -playerManagementRoutes.get('/stats', playerController.getStats); +playerRoutes.get('/stats', playerController.getStats); -playerManagementRoutes.put('/settings', +playerRoutes.put('/settings', validateRequest(require('joi').object({ // TODO: Define settings schema notifications: require('joi').object({ @@ -217,19 +138,19 @@ playerManagementRoutes.put('/settings', playerController.updateSettings ); -playerManagementRoutes.get('/activity', +playerRoutes.get('/activity', validators.validatePagination, playerController.getActivity ); -playerManagementRoutes.get('/notifications', +playerRoutes.get('/notifications', validateRequest(require('joi').object({ unreadOnly: require('joi').boolean().default(false) }), 'query'), playerController.getNotifications ); -playerManagementRoutes.put('/notifications/read', +playerRoutes.put('/notifications/read', validateRequest(require('joi').object({ notificationIds: require('joi').array().items( require('joi').number().integer().positive() @@ -238,36 +159,174 @@ playerManagementRoutes.put('/notifications/read', playerController.markNotificationsRead ); -// Mount player management routes (separate from game feature routes) -router.use('/player', playerManagementRoutes); - -/** - * Combat Routes - * /api/combat/* - */ -router.use('/combat', require('./api/combat')); +// Mount player routes +router.use('/player', playerRoutes); /** * Game Feature Routes - * Connect to existing working player route modules + * These will be expanded with actual game functionality */ -// Import existing player route modules for game features -const playerGameRoutes = require('./player'); +// Colonies Routes (placeholder) +router.get('/colonies', + authenticatePlayer, + validators.validatePagination, + (req, res) => { + res.json({ + success: true, + message: 'Colonies endpoint - feature not yet implemented', + data: { + colonies: [], + pagination: { + page: 1, + limit: 20, + total: 0, + totalPages: 0 + } + }, + correlationId: req.correlationId + }); + } +); -// Mount player game routes under /player-game prefix to avoid conflicts -// These contain the actual game functionality (colonies, resources, fleets, etc.) -router.use('/player-game', playerGameRoutes); +router.post('/colonies', + authenticatePlayer, + rateLimiters.gameAction, + validators.validateColonyCreation, + (req, res) => { + res.status(501).json({ + success: false, + message: 'Colony creation feature not yet implemented', + correlationId: req.correlationId + }); + } +); -// Direct mount of specific game features for convenience (these are duplicates of what's in /player/*) -// These provide direct access without the /player prefix for backwards compatibility -router.use('/colonies', authenticatePlayerToken, require('./player/colonies')); -router.use('/resources', authenticatePlayerToken, require('./player/resources')); -router.use('/fleets', authenticatePlayerToken, require('./player/fleets')); -router.use('/research', authenticatePlayerToken, require('./player/research')); -router.use('/galaxy', optionalPlayerToken, require('./player/galaxy')); -router.use('/notifications', authenticatePlayerToken, require('./player/notifications')); -router.use('/events', authenticatePlayerToken, require('./player/events')); +// Fleets Routes (placeholder) +router.get('/fleets', + authenticatePlayer, + validators.validatePagination, + (req, res) => { + res.json({ + success: true, + message: 'Fleets endpoint - feature not yet implemented', + data: { + fleets: [], + pagination: { + page: 1, + limit: 20, + total: 0, + totalPages: 0 + } + }, + correlationId: req.correlationId + }); + } +); + +router.post('/fleets', + authenticatePlayer, + rateLimiters.gameAction, + validators.validateFleetCreation, + (req, res) => { + res.status(501).json({ + success: false, + message: 'Fleet creation feature not yet implemented', + correlationId: req.correlationId + }); + } +); + +// Research Routes (placeholder) +router.get('/research', + authenticatePlayer, + (req, res) => { + res.json({ + success: true, + message: 'Research endpoint - feature not yet implemented', + data: { + currentResearch: null, + availableResearch: [], + completedResearch: [] + }, + correlationId: req.correlationId + }); + } +); + +router.post('/research', + authenticatePlayer, + rateLimiters.gameAction, + validators.validateResearchInitiation, + (req, res) => { + res.status(501).json({ + success: false, + message: 'Research initiation feature not yet implemented', + correlationId: req.correlationId + }); + } +); + +// Galaxy Routes (placeholder) +router.get('/galaxy', + authenticatePlayer, + validateRequest(require('joi').object({ + sector: require('joi').string().pattern(/^[A-Z]\d+$/).optional(), + coordinates: require('joi').string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).optional() + }), 'query'), + (req, res) => { + const { sector, coordinates } = req.query; + + res.json({ + success: true, + message: 'Galaxy endpoint - feature not yet implemented', + data: { + sector: sector || null, + coordinates: coordinates || null, + systems: [], + playerColonies: [], + playerFleets: [] + }, + correlationId: req.correlationId + }); + } +); + +// Messages Routes (placeholder) +router.get('/messages', + authenticatePlayer, + validators.validatePagination, + (req, res) => { + res.json({ + success: true, + message: 'Messages endpoint - feature not yet implemented', + data: { + messages: [], + unreadCount: 0, + pagination: { + page: 1, + limit: 20, + total: 0, + totalPages: 0 + } + }, + correlationId: req.correlationId + }); + } +); + +router.post('/messages', + authenticatePlayer, + rateLimiters.messaging, + validators.validateMessageSend, + (req, res) => { + res.status(501).json({ + success: false, + message: 'Message sending feature not yet implemented', + correlationId: req.correlationId + }); + } +); /** * Error handling for API routes diff --git a/src/routes/api/combat.js b/src/routes/api/combat.js deleted file mode 100644 index c0aed09..0000000 --- a/src/routes/api/combat.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Combat API Routes - * Defines all combat-related endpoints for players - */ - -const express = require('express'); -const router = express.Router(); - -// Import controllers -const { - initiateCombat, - getActiveCombats, - getCombatHistory, - getCombatEncounter, - getCombatStatistics, - updateFleetPosition, - getCombatTypes, - forceResolveCombat, -} = require('../../controllers/api/combat.controller'); - -// Import middleware -const { authenticatePlayer } = require('../../middleware/auth.middleware'); -const { - validateCombatInitiation, - validateFleetPositionUpdate, - validateCombatHistoryQuery, - validateParams, - checkFleetOwnership, - checkBattleAccess, - checkCombatCooldown, - checkFleetAvailability, - combatRateLimit, - logCombatAction, -} = require('../../middleware/combat.middleware'); - -// Apply authentication to all combat routes -router.use(authenticatePlayer); - -/** - * @route POST /api/combat/initiate - * @desc Initiate combat between fleets or fleet vs colony - * @access Private - */ -router.post('/initiate', - logCombatAction('initiate_combat'), - combatRateLimit(5, 15), // Max 5 combat initiations per 15 minutes - checkCombatCooldown, - validateCombatInitiation, - checkFleetAvailability, - initiateCombat, -); - -/** - * @route GET /api/combat/active - * @desc Get active combats for the current player - * @access Private - */ -router.get('/active', - logCombatAction('get_active_combats'), - getActiveCombats, -); - -/** - * @route GET /api/combat/history - * @desc Get combat history for the current player - * @access Private - */ -router.get('/history', - logCombatAction('get_combat_history'), - validateCombatHistoryQuery, - getCombatHistory, -); - -/** - * @route GET /api/combat/encounter/:encounterId - * @desc Get detailed combat encounter information - * @access Private - */ -router.get('/encounter/:encounterId', - logCombatAction('get_combat_encounter'), - validateParams('encounterId'), - getCombatEncounter, -); - -/** - * @route GET /api/combat/statistics - * @desc Get combat statistics for the current player - * @access Private - */ -router.get('/statistics', - logCombatAction('get_combat_statistics'), - getCombatStatistics, -); - -/** - * @route PUT /api/combat/position/:fleetId - * @desc Update fleet positioning for tactical combat - * @access Private - */ -router.put('/position/:fleetId', - logCombatAction('update_fleet_position'), - validateParams('fleetId'), - checkFleetOwnership, - validateFleetPositionUpdate, - updateFleetPosition, -); - -/** - * @route GET /api/combat/types - * @desc Get available combat types and configurations - * @access Private - */ -router.get('/types', - logCombatAction('get_combat_types'), - getCombatTypes, -); - -/** - * @route POST /api/combat/resolve/:battleId - * @desc Force resolve a combat (emergency use only) - * @access Private (requires special permission) - */ -router.post('/resolve/:battleId', - logCombatAction('force_resolve_combat'), - validateParams('battleId'), - checkBattleAccess, - forceResolveCombat, -); - -module.exports = router; diff --git a/src/routes/debug.js b/src/routes/debug.js index a682b8d..463ad8f 100644 --- a/src/routes/debug.js +++ b/src/routes/debug.js @@ -12,545 +12,303 @@ const logger = require('../utils/logger'); // Middleware to ensure debug routes are only available in development router.use((req, res, next) => { - if (process.env.NODE_ENV !== 'development') { - return res.status(404).json({ - error: 'Debug endpoints not available in production', - }); - } - next(); + if (process.env.NODE_ENV !== 'development') { + return res.status(404).json({ + error: 'Debug endpoints not available in production' + }); + } + next(); }); /** * Debug API Information */ router.get('/', (req, res) => { - res.json({ - name: 'Shattered Void - Debug API', - environment: process.env.NODE_ENV, - timestamp: new Date().toISOString(), - correlationId: req.correlationId, - endpoints: { - database: '/debug/database', - redis: '/debug/redis', - websocket: '/debug/websocket', - system: '/debug/system', - logs: '/debug/logs', - player: '/debug/player/:playerId', - colonies: '/debug/colonies', - resources: '/debug/resources', - gameEvents: '/debug/game-events', - }, - }); + res.json({ + name: 'Shattered Void - Debug API', + environment: process.env.NODE_ENV, + timestamp: new Date().toISOString(), + correlationId: req.correlationId, + endpoints: { + database: '/debug/database', + redis: '/debug/redis', + websocket: '/debug/websocket', + system: '/debug/system', + logs: '/debug/logs', + player: '/debug/player/:playerId' + } + }); }); /** * Database Debug Information */ router.get('/database', async (req, res) => { - try { - // Test database connection - const dbTest = await db.raw('SELECT NOW() as current_time, version() as db_version'); - - // Get table information - const tables = await db.raw(` + try { + // Test database connection + const dbTest = await db.raw('SELECT NOW() as current_time, version() as db_version'); + + // Get table information + const tables = await db.raw(` SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema = ? AND table_type = 'BASE TABLE' `, [process.env.DB_NAME || 'shattered_void_dev']); - res.json({ - status: 'connected', - connection: { - host: process.env.DB_HOST, - database: process.env.DB_NAME, - currentTime: dbTest.rows[0].current_time, - version: dbTest.rows[0].db_version, - }, - tables: tables.rows, - correlationId: req.correlationId, - }); + res.json({ + status: 'connected', + connection: { + host: process.env.DB_HOST, + database: process.env.DB_NAME, + currentTime: dbTest.rows[0].current_time, + version: dbTest.rows[0].db_version + }, + tables: tables.rows, + correlationId: req.correlationId + }); - } catch (error) { - logger.error('Database debug error:', error); - res.status(500).json({ - status: 'error', - error: error.message, - correlationId: req.correlationId, - }); - } + } catch (error) { + logger.error('Database debug error:', error); + res.status(500).json({ + status: 'error', + error: error.message, + correlationId: req.correlationId + }); + } }); /** * Redis Debug Information */ router.get('/redis', async (req, res) => { - try { - const redisClient = getRedisClient(); + try { + const redisClient = getRedisClient(); + + if (!redisClient) { + return res.json({ + status: 'not_connected', + message: 'Redis client not available', + correlationId: req.correlationId + }); + } - if (!redisClient) { - return res.json({ - status: 'not_connected', - message: 'Redis client not available', - correlationId: req.correlationId, - }); + // Test Redis connection + const pong = await redisClient.ping(); + const info = await redisClient.info(); + + res.json({ + status: 'connected', + ping: pong, + info: info.split('\r\n').slice(0, 20), // First 20 lines of info + correlationId: req.correlationId + }); + + } catch (error) { + logger.error('Redis debug error:', error); + res.status(500).json({ + status: 'error', + error: error.message, + correlationId: req.correlationId + }); } - - // Test Redis connection - const pong = await redisClient.ping(); - const info = await redisClient.info(); - - res.json({ - status: 'connected', - ping: pong, - info: info.split('\r\n').slice(0, 20), // First 20 lines of info - correlationId: req.correlationId, - }); - - } catch (error) { - logger.error('Redis debug error:', error); - res.status(500).json({ - status: 'error', - error: error.message, - correlationId: req.correlationId, - }); - } }); /** * WebSocket Debug Information */ router.get('/websocket', (req, res) => { - try { - const io = getWebSocketServer(); - const stats = getConnectionStats(); + try { + const io = getWebSocketServer(); + const stats = getConnectionStats(); - if (!io) { - return res.json({ - status: 'not_initialized', - message: 'WebSocket server not available', - correlationId: req.correlationId, - }); + if (!io) { + return res.json({ + status: 'not_initialized', + message: 'WebSocket server not available', + correlationId: req.correlationId + }); + } + + res.json({ + status: 'running', + stats, + sockets: { + count: io.sockets.sockets.size, + rooms: Array.from(io.sockets.adapter.rooms.keys()) + }, + correlationId: req.correlationId + }); + + } catch (error) { + logger.error('WebSocket debug error:', error); + res.status(500).json({ + status: 'error', + error: error.message, + correlationId: req.correlationId + }); } - - res.json({ - status: 'running', - stats, - sockets: { - count: io.sockets.sockets.size, - rooms: Array.from(io.sockets.adapter.rooms.keys()), - }, - correlationId: req.correlationId, - }); - - } catch (error) { - logger.error('WebSocket debug error:', error); - res.status(500).json({ - status: 'error', - error: error.message, - correlationId: req.correlationId, - }); - } }); /** * System Debug Information */ router.get('/system', (req, res) => { - const memUsage = process.memoryUsage(); - const cpuUsage = process.cpuUsage(); + const memUsage = process.memoryUsage(); + const cpuUsage = process.cpuUsage(); - res.json({ - process: { - pid: process.pid, - uptime: process.uptime(), - version: process.version, - platform: process.platform, - arch: process.arch, - }, - memory: { - rss: Math.round(memUsage.rss / 1024 / 1024), - heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024), - heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), - external: Math.round(memUsage.external / 1024 / 1024), - }, - cpu: { - user: cpuUsage.user, - system: cpuUsage.system, - }, - environment: { - nodeEnv: process.env.NODE_ENV, - port: process.env.PORT, - logLevel: process.env.LOG_LEVEL, - }, - correlationId: req.correlationId, - }); + res.json({ + process: { + pid: process.pid, + uptime: process.uptime(), + version: process.version, + platform: process.platform, + arch: process.arch + }, + memory: { + rss: Math.round(memUsage.rss / 1024 / 1024), + heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024), + heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), + external: Math.round(memUsage.external / 1024 / 1024) + }, + cpu: { + user: cpuUsage.user, + system: cpuUsage.system + }, + environment: { + nodeEnv: process.env.NODE_ENV, + port: process.env.PORT, + logLevel: process.env.LOG_LEVEL + }, + correlationId: req.correlationId + }); }); /** * Recent Logs Debug Information */ router.get('/logs', (req, res) => { - const { level = 'info', limit = 50 } = req.query; + const { level = 'info', limit = 50 } = req.query; - // Note: This is a placeholder. In a real implementation, - // you'd want to read from your log files or log storage system - res.json({ - message: 'Log retrieval not implemented', - note: 'This would show recent log entries filtered by level', - requested: { - level, - limit: parseInt(limit), - }, - suggestion: 'Check log files directly in logs/ directory', - correlationId: req.correlationId, - }); + // Note: This is a placeholder. In a real implementation, + // you'd want to read from your log files or log storage system + res.json({ + message: 'Log retrieval not implemented', + note: 'This would show recent log entries filtered by level', + requested: { + level, + limit: parseInt(limit) + }, + suggestion: 'Check log files directly in logs/ directory', + correlationId: req.correlationId + }); }); /** * Player Debug Information */ router.get('/player/:playerId', async (req, res) => { - try { - const playerId = parseInt(req.params.playerId); + try { + const playerId = parseInt(req.params.playerId); - if (isNaN(playerId)) { - return res.status(400).json({ - error: 'Invalid player ID', - correlationId: req.correlationId, - }); + if (isNaN(playerId)) { + return res.status(400).json({ + error: 'Invalid player ID', + correlationId: req.correlationId + }); + } + + // Get comprehensive player information + const player = await db('players') + .where('id', playerId) + .first(); + + if (!player) { + return res.status(404).json({ + error: 'Player not found', + correlationId: req.correlationId + }); + } + + const resources = await db('player_resources') + .where('player_id', playerId) + .first(); + + const stats = await db('player_stats') + .where('player_id', playerId) + .first(); + + const colonies = await db('colonies') + .where('player_id', playerId) + .select(['id', 'name', 'coordinates', 'created_at']); + + const fleets = await db('fleets') + .where('player_id', playerId) + .select(['id', 'name', 'status', 'created_at']); + + // Remove sensitive information + delete player.password_hash; + + res.json({ + player, + resources, + stats, + colonies, + fleets, + summary: { + totalColonies: colonies.length, + totalFleets: fleets.length, + accountAge: Math.floor((Date.now() - new Date(player.created_at).getTime()) / (1000 * 60 * 60 * 24)) + }, + correlationId: req.correlationId + }); + + } catch (error) { + logger.error('Player debug error:', error); + res.status(500).json({ + error: error.message, + correlationId: req.correlationId + }); } - - // Get comprehensive player information - const player = await db('players') - .where('id', playerId) - .first(); - - if (!player) { - return res.status(404).json({ - error: 'Player not found', - correlationId: req.correlationId, - }); - } - - const resources = await db('player_resources') - .where('player_id', playerId) - .first(); - - const stats = await db('player_stats') - .where('player_id', playerId) - .first(); - - const colonies = await db('colonies') - .where('player_id', playerId) - .select(['id', 'name', 'coordinates', 'created_at']); - - const fleets = await db('fleets') - .where('player_id', playerId) - .select(['id', 'name', 'status', 'created_at']); - - // Remove sensitive information - delete player.password_hash; - - res.json({ - player, - resources, - stats, - colonies, - fleets, - summary: { - totalColonies: colonies.length, - totalFleets: fleets.length, - accountAge: Math.floor((Date.now() - new Date(player.created_at).getTime()) / (1000 * 60 * 60 * 24)), - }, - correlationId: req.correlationId, - }); - - } catch (error) { - logger.error('Player debug error:', error); - res.status(500).json({ - error: error.message, - correlationId: req.correlationId, - }); - } }); /** * Test Endpoint for Various Scenarios */ router.get('/test/:scenario', (req, res) => { - const { scenario } = req.params; + const { scenario } = req.params; - switch (scenario) { - case 'error': - throw new Error('Test error for debugging'); - - case 'slow': - setTimeout(() => { - res.json({ - message: 'Slow response test completed', - delay: '3 seconds', - correlationId: req.correlationId, - }); - }, 3000); - break; - - case 'memory': - // Create a large object to test memory usage - const largeArray = new Array(1000000).fill('test data'); - res.json({ - message: 'Memory test completed', - arrayLength: largeArray.length, - correlationId: req.correlationId, - }); - break; - - default: - res.json({ - message: 'Test scenario not recognized', - availableScenarios: ['error', 'slow', 'memory'], - correlationId: req.correlationId, - }); - } -}); - -/** - * Colony Debug Information - */ -router.get('/colonies', async (req, res) => { - try { - const { playerId, limit = 10 } = req.query; - - let query = db('colonies') - .select([ - 'colonies.*', - 'planet_types.name as planet_type_name', - 'galaxy_sectors.name as sector_name', - 'players.username', - ]) - .leftJoin('planet_types', 'colonies.planet_type_id', 'planet_types.id') - .leftJoin('galaxy_sectors', 'colonies.sector_id', 'galaxy_sectors.id') - .leftJoin('players', 'colonies.player_id', 'players.id') - .orderBy('colonies.founded_at', 'desc') - .limit(parseInt(limit)); - - if (playerId) { - query = query.where('colonies.player_id', parseInt(playerId)); + switch (scenario) { + case 'error': + throw new Error('Test error for debugging'); + + case 'slow': + setTimeout(() => { + res.json({ + message: 'Slow response test completed', + delay: '3 seconds', + correlationId: req.correlationId + }); + }, 3000); + break; + + case 'memory': + // Create a large object to test memory usage + const largeArray = new Array(1000000).fill('test data'); + res.json({ + message: 'Memory test completed', + arrayLength: largeArray.length, + correlationId: req.correlationId + }); + break; + + default: + res.json({ + message: 'Test scenario not recognized', + availableScenarios: ['error', 'slow', 'memory'], + correlationId: req.correlationId + }); } - - const colonies = await query; - - // Get building counts for each colony - const coloniesWithBuildings = await Promise.all(colonies.map(async (colony) => { - const buildingCount = await db('colony_buildings') - .where('colony_id', colony.id) - .count('* as count') - .first(); - - const resourceProduction = await db('colony_resource_production') - .select([ - 'resource_types.name as resource_name', - 'colony_resource_production.production_rate', - 'colony_resource_production.current_stored', - ]) - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colony_resource_production.colony_id', colony.id) - .where('colony_resource_production.production_rate', '>', 0); - - return { - ...colony, - buildingCount: parseInt(buildingCount.count) || 0, - resourceProduction, - }; - })); - - res.json({ - colonies: coloniesWithBuildings, - totalCount: coloniesWithBuildings.length, - filters: { playerId, limit }, - correlationId: req.correlationId, - }); - - } catch (error) { - logger.error('Colony debug error:', error); - res.status(500).json({ - error: error.message, - correlationId: req.correlationId, - }); - } }); -/** - * Resource Debug Information - */ -router.get('/resources', async (req, res) => { - try { - const { playerId } = req.query; - - // Get resource types - const resourceTypes = await db('resource_types') - .where('is_active', true) - .orderBy('category') - .orderBy('name'); - - const resourceSummary = {}; - - if (playerId) { - // Get specific player resources - const playerResources = await db('player_resources') - .select([ - 'player_resources.*', - 'resource_types.name as resource_name', - 'resource_types.category', - ]) - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', parseInt(playerId)); - - resourceSummary.playerResources = playerResources; - - // Get player's colony resource production - const colonyProduction = await db('colony_resource_production') - .select([ - 'colonies.name as colony_name', - 'resource_types.name as resource_name', - 'colony_resource_production.production_rate', - 'colony_resource_production.current_stored', - ]) - .join('colonies', 'colony_resource_production.colony_id', 'colonies.id') - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colonies.player_id', parseInt(playerId)) - .where('colony_resource_production.production_rate', '>', 0); - - resourceSummary.colonyProduction = colonyProduction; - } else { - // Get global resource statistics - const totalResources = await db('player_resources') - .select([ - 'resource_types.name as resource_name', - db.raw('SUM(player_resources.amount) as total_amount'), - db.raw('COUNT(player_resources.id) as player_count'), - db.raw('AVG(player_resources.amount) as average_amount'), - ]) - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .groupBy('resource_types.id', 'resource_types.name') - .orderBy('resource_types.name'); - - resourceSummary.globalStats = totalResources; - } - - res.json({ - resourceTypes, - ...resourceSummary, - filters: { playerId }, - correlationId: req.correlationId, - }); - - } catch (error) { - logger.error('Resource debug error:', error); - res.status(500).json({ - error: error.message, - correlationId: req.correlationId, - }); - } -}); - -/** - * Game Events Debug Information - */ -router.get('/game-events', (req, res) => { - try { - const serviceLocator = require('../services/ServiceLocator'); - const gameEventService = serviceLocator.get('gameEventService'); - - if (!gameEventService) { - return res.json({ - status: 'not_available', - message: 'Game event service not initialized', - correlationId: req.correlationId, - }); - } - - const connectedPlayers = gameEventService.getConnectedPlayerCount(); - - // Get room information - const io = gameEventService.io; - const rooms = Array.from(io.sockets.adapter.rooms.entries()).map(([roomName, socketSet]) => ({ - name: roomName, - socketCount: socketSet.size, - type: roomName.includes(':') ? roomName.split(':')[0] : 'unknown', - })); - - res.json({ - status: 'active', - connectedPlayers, - rooms: { - total: rooms.length, - breakdown: rooms, - }, - eventTypes: [ - 'colony_created', - 'building_constructed', - 'resources_updated', - 'resource_production', - 'colony_status_update', - 'error', - 'notification', - 'player_status_change', - 'system_announcement', - ], - correlationId: req.correlationId, - }); - - } catch (error) { - logger.error('Game events debug error:', error); - res.status(500).json({ - error: error.message, - correlationId: req.correlationId, - }); - } -}); - -/** - * Add resources to a player (for testing) - */ -router.post('/add-resources', async (req, res) => { - try { - const { playerId, resources } = req.body; - - if (!playerId || !resources) { - return res.status(400).json({ - error: 'playerId and resources are required', - correlationId: req.correlationId, - }); - } - - const serviceLocator = require('../services/ServiceLocator'); - const ResourceService = require('../services/resource/ResourceService'); - const gameEventService = serviceLocator.get('gameEventService'); - const resourceService = new ResourceService(gameEventService); - - const updatedResources = await resourceService.addPlayerResources( - playerId, - resources, - req.correlationId, - ); - - res.json({ - success: true, - message: 'Resources added successfully', - playerId, - addedResources: resources, - updatedResources, - correlationId: req.correlationId, - }); - - } catch (error) { - logger.error('Add resources debug error:', error); - res.status(500).json({ - error: error.message, - correlationId: req.correlationId, - }); - } -}); - -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index b6667ce..6bec20b 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -16,111 +16,111 @@ const adminRoutes = require('./admin'); * Root endpoint - API information */ router.get('/', (req, res) => { - const apiInfo = { - name: 'Shattered Void MMO API', - version: process.env.npm_package_version || '0.1.0', - environment: process.env.NODE_ENV || 'development', - status: 'operational', - timestamp: new Date().toISOString(), - endpoints: { - health: '/health', - api: '/api', - admin: '/api/admin', - }, - documentation: { - api: '/docs/api', - admin: '/docs/admin', - }, - correlationId: req.correlationId, - }; + const apiInfo = { + name: 'Shattered Void MMO API', + version: process.env.npm_package_version || '0.1.0', + environment: process.env.NODE_ENV || 'development', + status: 'operational', + timestamp: new Date().toISOString(), + endpoints: { + health: '/health', + api: '/api', + admin: '/api/admin' + }, + documentation: { + api: '/docs/api', + admin: '/docs/admin' + }, + correlationId: req.correlationId + }; - res.json(apiInfo); + res.json(apiInfo); }); /** * API Documentation endpoint (placeholder) */ router.get('/docs', (req, res) => { - res.json({ - message: 'API Documentation', - note: 'Interactive API documentation will be available here', - version: process.env.npm_package_version || '0.1.0', - correlationId: req.correlationId, - links: { - playerAPI: '/docs/api', - adminAPI: '/docs/admin', - }, - }); + res.json({ + message: 'API Documentation', + note: 'Interactive API documentation will be available here', + version: process.env.npm_package_version || '0.1.0', + correlationId: req.correlationId, + links: { + playerAPI: '/docs/api', + adminAPI: '/docs/admin' + } + }); }); /** * Player API Documentation (placeholder) */ router.get('/docs/api', (req, res) => { - res.json({ - title: 'Shattered Void - Player API Documentation', - version: process.env.npm_package_version || '0.1.0', - description: 'API endpoints for player operations', - baseUrl: '/api', - correlationId: req.correlationId, - endpoints: { - authentication: { - register: 'POST /api/auth/register', - login: 'POST /api/auth/login', - logout: 'POST /api/auth/logout', - profile: 'GET /api/auth/me', - updateProfile: 'PUT /api/auth/me', - verify: 'GET /api/auth/verify', - }, - player: { - dashboard: 'GET /api/player/dashboard', - resources: 'GET /api/player/resources', - stats: 'GET /api/player/stats', - notifications: 'GET /api/player/notifications', - }, - game: { - colonies: 'GET /api/colonies', - fleets: 'GET /api/fleets', - research: 'GET /api/research', - galaxy: 'GET /api/galaxy', - }, - }, - note: 'Full interactive documentation coming soon', - }); + res.json({ + title: 'Shattered Void - Player API Documentation', + version: process.env.npm_package_version || '0.1.0', + description: 'API endpoints for player operations', + baseUrl: '/api', + correlationId: req.correlationId, + endpoints: { + authentication: { + register: 'POST /api/auth/register', + login: 'POST /api/auth/login', + logout: 'POST /api/auth/logout', + profile: 'GET /api/auth/me', + updateProfile: 'PUT /api/auth/me', + verify: 'GET /api/auth/verify' + }, + player: { + dashboard: 'GET /api/player/dashboard', + resources: 'GET /api/player/resources', + stats: 'GET /api/player/stats', + notifications: 'GET /api/player/notifications' + }, + game: { + colonies: 'GET /api/colonies', + fleets: 'GET /api/fleets', + research: 'GET /api/research', + galaxy: 'GET /api/galaxy' + } + }, + note: 'Full interactive documentation coming soon' + }); }); /** * Admin API Documentation (placeholder) */ router.get('/docs/admin', (req, res) => { - res.json({ - title: 'Shattered Void - Admin API Documentation', - version: process.env.npm_package_version || '0.1.0', - description: 'API endpoints for administrative operations', - baseUrl: '/api/admin', - correlationId: req.correlationId, - endpoints: { - authentication: { - login: 'POST /api/admin/auth/login', - logout: 'POST /api/admin/auth/logout', - profile: 'GET /api/admin/auth/me', - verify: 'GET /api/admin/auth/verify', - stats: 'GET /api/admin/auth/stats', - }, - playerManagement: { - listPlayers: 'GET /api/admin/players', - getPlayer: 'GET /api/admin/players/:id', - updatePlayer: 'PUT /api/admin/players/:id', - deactivatePlayer: 'DELETE /api/admin/players/:id', - }, - systemManagement: { - systemStats: 'GET /api/admin/system/stats', - events: 'GET /api/admin/events', - analytics: 'GET /api/admin/analytics', - }, - }, - note: 'Full interactive documentation coming soon', - }); + res.json({ + title: 'Shattered Void - Admin API Documentation', + version: process.env.npm_package_version || '0.1.0', + description: 'API endpoints for administrative operations', + baseUrl: '/api/admin', + correlationId: req.correlationId, + endpoints: { + authentication: { + login: 'POST /api/admin/auth/login', + logout: 'POST /api/admin/auth/logout', + profile: 'GET /api/admin/auth/me', + verify: 'GET /api/admin/auth/verify', + stats: 'GET /api/admin/auth/stats' + }, + playerManagement: { + listPlayers: 'GET /api/admin/players', + getPlayer: 'GET /api/admin/players/:id', + updatePlayer: 'PUT /api/admin/players/:id', + deactivatePlayer: 'DELETE /api/admin/players/:id' + }, + systemManagement: { + systemStats: 'GET /api/admin/system/stats', + events: 'GET /api/admin/events', + analytics: 'GET /api/admin/analytics' + } + }, + note: 'Full interactive documentation coming soon' + }); }); // Mount route modules @@ -128,17 +128,17 @@ router.use('/api', apiRoutes); // Admin routes (if enabled) if (process.env.ENABLE_ADMIN_ROUTES !== 'false') { - router.use('/api/admin', adminRoutes); - logger.info('Admin routes enabled'); + router.use('/api/admin', adminRoutes); + logger.info('Admin routes enabled'); } else { - logger.info('Admin routes disabled'); + logger.info('Admin routes disabled'); } // Debug routes (development only) if (process.env.NODE_ENV === 'development' && process.env.ENABLE_DEBUG_ENDPOINTS === 'true') { - const debugRoutes = require('./debug'); - router.use('/debug', debugRoutes); - logger.info('Debug routes enabled'); + const debugRoutes = require('./debug'); + router.use('/debug', debugRoutes); + logger.info('Debug routes enabled'); } -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/src/routes/player/auth.js b/src/routes/player/auth.js index 083c5da..cf7d3ae 100644 --- a/src/routes/player/auth.js +++ b/src/routes/player/auth.js @@ -64,4 +64,4 @@ router.post('/reset-password', asyncHandler(async (req, res) => { }); })); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/src/routes/player/colonies.js b/src/routes/player/colonies.js index 84265ad..e69de29 100644 --- a/src/routes/player/colonies.js +++ b/src/routes/player/colonies.js @@ -1,53 +0,0 @@ -/** - * Player Colony Routes - * Handles all colony-related endpoints for players - */ - -const express = require('express'); -const router = express.Router(); - -const { - createColony, - getPlayerColonies, - getColonyDetails, - constructBuilding, - getBuildingTypes, - getPlanetTypes, - getGalaxySectors, -} = require('../../controllers/player/colony.controller'); - -const { validateRequest } = require('../../middleware/validation.middleware'); -const { - createColonySchema, - constructBuildingSchema, - colonyIdParamSchema, -} = require('../../validators/colony.validators'); - -// Colony CRUD operations -router.post('/', - validateRequest(createColonySchema), - createColony, -); - -router.get('/', - getPlayerColonies, -); - -router.get('/:colonyId', - validateRequest(colonyIdParamSchema, 'params'), - getColonyDetails, -); - -// Building operations -router.post('/:colonyId/buildings', - validateRequest(colonyIdParamSchema, 'params'), - validateRequest(constructBuildingSchema), - constructBuilding, -); - -// Reference data endpoints -router.get('/ref/building-types', getBuildingTypes); -router.get('/ref/planet-types', getPlanetTypes); -router.get('/ref/galaxy-sectors', getGalaxySectors); - -module.exports = router; diff --git a/src/routes/player/events.js b/src/routes/player/events.js index def555f..e69de29 100644 --- a/src/routes/player/events.js +++ b/src/routes/player/events.js @@ -1,33 +0,0 @@ -/** - * Player Events Routes - * Handles player event history and notifications - */ - -const express = require('express'); -const router = express.Router(); - -// TODO: Implement events routes -router.get('/', (req, res) => { - res.json({ - message: 'Events routes not yet implemented', - available_endpoints: { - '/history': 'Get event history', - '/recent': 'Get recent events', - '/unread': 'Get unread events' - } - }); -}); - -router.get('/history', (req, res) => { - res.json({ message: 'Event history endpoint not implemented' }); -}); - -router.get('/recent', (req, res) => { - res.json({ message: 'Recent events endpoint not implemented' }); -}); - -router.get('/unread', (req, res) => { - res.json({ message: 'Unread events endpoint not implemented' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/player/fleets.js b/src/routes/player/fleets.js index b822c4d..e69de29 100644 --- a/src/routes/player/fleets.js +++ b/src/routes/player/fleets.js @@ -1,36 +0,0 @@ -/** - * Player Fleet Routes - * Handles fleet management and operations - */ - -const express = require('express'); -const router = express.Router(); -const fleetController = require('../../controllers/api/fleet.controller'); - -// Fleet management routes -router.get('/', fleetController.getPlayerFleets); -router.post('/', fleetController.createFleet); -router.get('/:fleetId', fleetController.getFleetDetails); -router.delete('/:fleetId', fleetController.disbandFleet); - -// Fleet operations -router.post('/:fleetId/move', fleetController.moveFleet); - -// TODO: Combat operations (will be implemented when combat system is enhanced) -router.post('/:fleetId/attack', (req, res) => { - res.status(501).json({ - success: false, - error: 'Not implemented', - message: 'Fleet combat operations will be available in a future update' - }); -}); - -// Ship design routes -router.get('/ship-designs/classes', fleetController.getShipClassesInfo); -router.get('/ship-designs/:designId', fleetController.getShipDesignDetails); -router.get('/ship-designs', fleetController.getAvailableShipDesigns); - -// Ship construction validation -router.post('/validate-construction', fleetController.validateShipConstruction); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/player/galaxy.js b/src/routes/player/galaxy.js index 8c05512..e69de29 100644 --- a/src/routes/player/galaxy.js +++ b/src/routes/player/galaxy.js @@ -1,33 +0,0 @@ -/** - * Player Galaxy Routes - * Handles galaxy exploration and sector viewing - */ - -const express = require('express'); -const router = express.Router(); - -// TODO: Implement galaxy routes -router.get('/', (req, res) => { - res.json({ - message: 'Galaxy routes not yet implemented', - available_endpoints: { - '/sectors': 'List galaxy sectors', - '/explore': 'Explore new areas', - '/map': 'View galaxy map' - } - }); -}); - -router.get('/sectors', (req, res) => { - res.json({ message: 'Galaxy sectors endpoint not implemented' }); -}); - -router.get('/explore', (req, res) => { - res.json({ message: 'Galaxy exploration endpoint not implemented' }); -}); - -router.get('/map', (req, res) => { - res.json({ message: 'Galaxy map endpoint not implemented' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/player/index.js b/src/routes/player/index.js index 3f44885..3713388 100644 --- a/src/routes/player/index.js +++ b/src/routes/player/index.js @@ -4,7 +4,7 @@ const express = require('express'); const { authenticateToken, optionalAuth } = require('../../middleware/auth'); -const { asyncHandler } = require('../../middleware/error.middleware'); +const { asyncHandler } = require('../../middleware/error-handler'); const router = express.Router(); @@ -12,7 +12,6 @@ const router = express.Router(); const authRoutes = require('./auth'); const profileRoutes = require('./profile'); const coloniesRoutes = require('./colonies'); -const resourcesRoutes = require('./resources'); const fleetsRoutes = require('./fleets'); const researchRoutes = require('./research'); const galaxyRoutes = require('./galaxy'); @@ -26,7 +25,6 @@ router.use('/galaxy', optionalAuth('player'), galaxyRoutes); // Protected routes (authentication required) router.use('/profile', authenticateToken('player'), profileRoutes); router.use('/colonies', authenticateToken('player'), coloniesRoutes); -router.use('/resources', authenticateToken('player'), resourcesRoutes); router.use('/fleets', authenticateToken('player'), fleetsRoutes); router.use('/research', authenticateToken('player'), researchRoutes); router.use('/events', authenticateToken('player'), eventsRoutes); @@ -47,4 +45,4 @@ router.get('/status', authenticateToken('player'), asyncHandler(async (req, res) }); })); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/src/routes/player/notifications.js b/src/routes/player/notifications.js index 83038f0..e69de29 100644 --- a/src/routes/player/notifications.js +++ b/src/routes/player/notifications.js @@ -1,33 +0,0 @@ -/** - * Player Notifications Routes - * Handles player notifications and messages - */ - -const express = require('express'); -const router = express.Router(); - -// TODO: Implement notifications routes -router.get('/', (req, res) => { - res.json({ - message: 'Notifications routes not yet implemented', - available_endpoints: { - '/unread': 'Get unread notifications', - '/all': 'Get all notifications', - '/mark-read': 'Mark notifications as read' - } - }); -}); - -router.get('/unread', (req, res) => { - res.json({ message: 'Unread notifications endpoint not implemented' }); -}); - -router.get('/all', (req, res) => { - res.json({ message: 'All notifications endpoint not implemented' }); -}); - -router.post('/mark-read', (req, res) => { - res.json({ message: 'Mark notifications read endpoint not implemented' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/player/profile.js b/src/routes/player/profile.js index ee39e17..e69de29 100644 --- a/src/routes/player/profile.js +++ b/src/routes/player/profile.js @@ -1,33 +0,0 @@ -/** - * Player Profile Routes - * Handles player profile management - */ - -const express = require('express'); -const router = express.Router(); - -// TODO: Implement profile routes -router.get('/', (req, res) => { - res.json({ - message: 'Profile routes not yet implemented', - available_endpoints: { - '/': 'Get player profile', - '/update': 'Update player profile', - '/settings': 'Get/update player settings' - } - }); -}); - -router.put('/', (req, res) => { - res.json({ message: 'Profile update endpoint not implemented' }); -}); - -router.get('/settings', (req, res) => { - res.json({ message: 'Profile settings endpoint not implemented' }); -}); - -router.put('/settings', (req, res) => { - res.json({ message: 'Profile settings update endpoint not implemented' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/player/research.js b/src/routes/player/research.js index b88d907..e69de29 100644 --- a/src/routes/player/research.js +++ b/src/routes/player/research.js @@ -1,67 +0,0 @@ -/** - * Player Research Routes - * Handles research and technology management - */ - -const express = require('express'); -const router = express.Router(); - -// Import controllers and middleware -const researchController = require('../../controllers/api/research.controller'); -const { - validateStartResearch, - validateTechnologyTreeFilter, - validateResearchStats -} = require('../../validators/research.validators'); - -/** - * Get current research status for the authenticated player - * GET /player/research/ - */ -router.get('/', researchController.getResearchStatus); - -/** - * Get available technologies for research - * GET /player/research/available - */ -router.get('/available', researchController.getAvailableTechnologies); - -/** - * Get completed technologies - * GET /player/research/completed - */ -router.get('/completed', researchController.getCompletedTechnologies); - -/** - * Get full technology tree with player progress - * GET /player/research/technology-tree - * Query params: category, tier, status, include_unavailable, sort_by, sort_order - */ -router.get('/technology-tree', - validateTechnologyTreeFilter, - researchController.getTechnologyTree -); - -/** - * Get research queue (current and queued research) - * GET /player/research/queue - */ -router.get('/queue', researchController.getResearchQueue); - -/** - * Start research on a technology - * POST /player/research/start - * Body: { technology_id: number } - */ -router.post('/start', - validateStartResearch, - researchController.startResearch -); - -/** - * Cancel current research - * POST /player/research/cancel - */ -router.post('/cancel', researchController.cancelResearch); - -module.exports = router; \ No newline at end of file diff --git a/src/routes/player/resources.js b/src/routes/player/resources.js deleted file mode 100644 index 61c9254..0000000 --- a/src/routes/player/resources.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Player Resource Routes - * Handles all resource-related endpoints for players - */ - -const express = require('express'); -const router = express.Router(); - -const { - getPlayerResources, - getPlayerResourceSummary, - getResourceProduction, - addResources, - transferResources, - getResourceTypes, -} = require('../../controllers/player/resource.controller'); - -const { validateRequest } = require('../../middleware/validation.middleware'); -const { - transferResourcesSchema, - addResourcesSchema, - resourceQuerySchema, -} = require('../../validators/resource.validators'); - -// Resource information endpoints -router.get('/', - validateRequest(resourceQuerySchema, 'query'), - getPlayerResources, -); - -router.get('/summary', - getPlayerResourceSummary, -); - -router.get('/production', - getResourceProduction, -); - -// Resource manipulation endpoints -router.post('/transfer', - validateRequest(transferResourcesSchema), - transferResources, -); - -// Development/testing endpoints -router.post('/add', - validateRequest(addResourcesSchema), - addResources, -); - -// Reference data endpoints -router.get('/types', getResourceTypes); - -module.exports = router; diff --git a/src/server.js b/src/server.js index 2c371c0..b12a586 100644 --- a/src/server.js +++ b/src/server.js @@ -16,7 +16,6 @@ const { initializeGameTick } = require('./services/game-tick.service'); // Configuration const PORT = process.env.PORT || 3000; -const HOST = process.env.HOST || '0.0.0.0'; const NODE_ENV = process.env.NODE_ENV || 'development'; // Global instances @@ -28,202 +27,175 @@ let io; * Initialize all core systems */ async function initializeSystems() { - try { - logger.info('Initializing core systems...'); + try { + logger.info('Initializing core systems...'); + + // Initialize database connections + if (process.env.DISABLE_DATABASE !== 'true') { + await initializeDatabase(); + logger.info('Database systems initialized'); + } else { + logger.warn('Database disabled by environment variable'); + } - // Initialize database connections - if (process.env.DISABLE_DATABASE !== 'true') { - await initializeDatabase(); - logger.info('Database systems initialized'); - } else { - logger.warn('Database disabled by environment variable'); + // Initialize Redis + if (process.env.DISABLE_REDIS !== 'true') { + await initializeRedis(); + logger.info('Redis systems initialized'); + } else { + logger.warn('Redis disabled by environment variable'); + } + + // Initialize WebSocket + io = await initializeWebSocket(server); + logger.info('WebSocket systems initialized'); + + // Initialize game systems + await initializeGameSystems(); + logger.info('Game systems initialized'); + + } catch (error) { + logger.error('Failed to initialize systems:', error); + throw error; } - - // Initialize Redis - if (process.env.DISABLE_REDIS !== 'true') { - await initializeRedis(); - logger.info('Redis systems initialized'); - } else { - logger.warn('Redis disabled by environment variable'); - } - - // Initialize WebSocket - io = await initializeWebSocket(server); - logger.info('WebSocket systems initialized'); - - // Initialize service locator with WebSocket service - const serviceLocator = require('./services/ServiceLocator'); - const GameEventService = require('./services/websocket/GameEventService'); - const gameEventService = new GameEventService(io); - serviceLocator.register('gameEventService', gameEventService); - - // Initialize fleet services - const FleetService = require('./services/fleet/FleetService'); - const ShipDesignService = require('./services/fleet/ShipDesignService'); - const shipDesignService = new ShipDesignService(gameEventService); - const fleetService = new FleetService(gameEventService, shipDesignService); - serviceLocator.register('shipDesignService', shipDesignService); - serviceLocator.register('fleetService', fleetService); - - // Initialize research services - const ResearchService = require('./services/research/ResearchService'); - const researchService = new ResearchService(gameEventService); - serviceLocator.register('researchService', researchService); - - logger.info('Service locator initialized with fleet and research services'); - - // Initialize game systems - await initializeGameSystems(); - logger.info('Game systems initialized'); - - } catch (error) { - logger.error('Failed to initialize systems:', error); - throw error; - } } /** * Initialize game systems (tick processing, etc.) */ async function initializeGameSystems() { - try { - // Initialize game tick system - if (process.env.ENABLE_GAME_TICK !== 'false') { - await initializeGameTick(); - logger.info('Game tick system initialized'); + try { + // Initialize game tick system + if (process.env.ENABLE_GAME_TICK !== 'false') { + await initializeGameTick(); + logger.info('Game tick system initialized'); + } + + // Add other game system initializations here + + } catch (error) { + logger.error('Game systems initialization failed:', error); + throw error; } - - // Add other game system initializations here - - } catch (error) { - logger.error('Game systems initialization failed:', error); - throw error; - } } /** * Graceful shutdown handler */ function setupGracefulShutdown() { - const shutdown = async (signal) => { - logger.info(`Received ${signal}. Starting graceful shutdown...`); + const shutdown = async (signal) => { + logger.info(`Received ${signal}. Starting graceful shutdown...`); - try { - // Stop accepting new connections - if (server) { - server.close(() => { - logger.info('HTTP server closed'); - }); - } - - // Close WebSocket connections - if (io) { - io.close(() => { - logger.info('WebSocket server closed'); - }); - } - - // Close database connections - const db = require('./database/connection'); - if (db) { - await db.destroy(); - logger.info('Database connections closed'); - } - - // Close Redis connection - if (process.env.DISABLE_REDIS !== 'true') { try { - const { closeRedis } = require('./config/redis'); - await closeRedis(); - logger.info('Redis connection closed'); + // Stop accepting new connections + if (server) { + server.close(() => { + logger.info('HTTP server closed'); + }); + } + + // Close WebSocket connections + if (io) { + io.close(() => { + logger.info('WebSocket server closed'); + }); + } + + // Close database connections + const db = require('./database/connection'); + if (db) { + await db.destroy(); + logger.info('Database connections closed'); + } + + // Close Redis connection + const redisConfig = require('./config/redis'); + if (redisConfig.client) { + await redisConfig.client.quit(); + logger.info('Redis connection closed'); + } + + logger.info('Graceful shutdown completed'); + process.exit(0); } catch (error) { - logger.warn('Error closing Redis connection (may already be closed):', error.message); + logger.error('Error during shutdown:', error); + process.exit(1); } - } else { - logger.info('Redis connection closure skipped - Redis was disabled'); - } + }; - logger.info('Graceful shutdown completed'); - process.exit(0); - } catch (error) { - logger.error('Error during shutdown:', error); - process.exit(1); - } - }; + // Handle shutdown signals + process.on('SIGTERM', () => shutdown('SIGTERM')); + process.on('SIGINT', () => shutdown('SIGINT')); - // Handle shutdown signals - process.on('SIGTERM', () => shutdown('SIGTERM')); - process.on('SIGINT', () => shutdown('SIGINT')); - - // Handle unhandled promise rejections - process.on('unhandledRejection', (reason, promise) => { - logger.error('Unhandled Promise Rejection:', { - reason: reason?.message || reason, - stack: reason?.stack, - promise: promise?.toString(), + // Handle unhandled promise rejections + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Promise Rejection:', { + reason: reason?.message || reason, + stack: reason?.stack, + promise: promise?.toString() + }); }); - }); - // Handle uncaught exceptions - process.on('uncaughtException', (error) => { - logger.error('Uncaught Exception:', { - message: error.message, - stack: error.stack, + // Handle uncaught exceptions + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', { + message: error.message, + stack: error.stack + }); + process.exit(1); }); - process.exit(1); - }); } /** * Start the application server */ async function startServer() { - try { - logger.info(`Starting Shattered Void MMO Server in ${NODE_ENV} mode...`); + try { + logger.info(`Starting Shattered Void MMO Server in ${NODE_ENV} mode...`); - // Create Express app - app = createApp(); + // Create Express app + app = createApp(); + + // Create HTTP server + server = http.createServer(app); - // Create HTTP server - server = http.createServer(app); + // Set up graceful shutdown handlers + setupGracefulShutdown(); - // Set up graceful shutdown handlers - setupGracefulShutdown(); + // Initialize all systems + await initializeSystems(); - // Initialize all systems - await initializeSystems(); + // Start the server + server.listen(PORT, () => { + logger.info(`Server running on port ${PORT}`); + logger.info(`Environment: ${NODE_ENV}`); + logger.info(`Process ID: ${process.pid}`); + + // Log memory usage + const memUsage = process.memoryUsage(); + logger.info('Initial memory usage:', { + rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`, + heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`, + heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`, + }); - // Start the server - server.listen(PORT, HOST, () => { - logger.info(`Server running on ${HOST}:${PORT}`); - logger.info(`Environment: ${NODE_ENV}`); - logger.info(`Process ID: ${process.pid}`); + logger.info('Shattered Void MMO Server started successfully'); + }); - // Log memory usage - const memUsage = process.memoryUsage(); - logger.info('Initial memory usage:', { - rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`, - heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`, - heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`, - }); - - logger.info('Shattered Void MMO Server started successfully'); - }); - - } catch (error) { - logger.error('Failed to start server:', error); - process.exit(1); - } + } catch (error) { + logger.error('Failed to start server:', error); + process.exit(1); + } } // Start the server if this file is run directly if (require.main === module) { - startServer(); + startServer(); } -module.exports = { - startServer, - getApp: () => app, - getServer: () => server, - getIO: () => io, -}; +module.exports = { + startServer, + getApp: () => app, + getServer: () => server, + getIO: () => io +}; \ No newline at end of file diff --git a/src/services/ServiceLocator.js b/src/services/ServiceLocator.js deleted file mode 100644 index cf93ea5..0000000 --- a/src/services/ServiceLocator.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Service Locator - * Manages service instances and dependency injection - */ - -class ServiceLocator { - constructor() { - this.services = new Map(); - } - - /** - * Register a service instance - * @param {string} name - Service name - * @param {Object} instance - Service instance - */ - register(name, instance) { - this.services.set(name, instance); - } - - /** - * Get a service instance - * @param {string} name - Service name - * @returns {Object} Service instance - */ - get(name) { - return this.services.get(name); - } - - /** - * Check if a service is registered - * @param {string} name - Service name - * @returns {boolean} True if service is registered - */ - has(name) { - return this.services.has(name); - } - - /** - * Clear all services - */ - clear() { - this.services.clear(); - } - - /** - * Get all registered service names - * @returns {Array} Array of service names - */ - getServiceNames() { - return Array.from(this.services.keys()); - } -} - -// Create singleton instance -const serviceLocator = new ServiceLocator(); - -module.exports = serviceLocator; diff --git a/src/services/auth/EmailService.js b/src/services/auth/EmailService.js deleted file mode 100644 index 722ab18..0000000 --- a/src/services/auth/EmailService.js +++ /dev/null @@ -1,420 +0,0 @@ -/** - * Email Service - * Handles email sending for authentication flows including verification and password reset - */ - -const nodemailer = require('nodemailer'); -const path = require('path'); -const fs = require('fs').promises; -const logger = require('../../utils/logger'); - -class EmailService { - constructor() { - this.transporter = null; - this.isDevelopment = process.env.NODE_ENV === 'development'; - this.initialize(); - } - - /** - * Initialize email transporter based on environment - */ - async initialize() { - try { - if (this.isDevelopment) { - // Development mode - log emails to console instead of sending - this.transporter = { - sendMail: async (mailOptions) => { - logger.info('📧 Email would be sent in production:', { - to: mailOptions.to, - subject: mailOptions.subject, - text: mailOptions.text?.substring(0, 200) + '...', - html: mailOptions.html ? 'HTML content included' : 'No HTML', - }); - return { messageId: `dev-${Date.now()}@localhost` }; - } - }; - logger.info('Email service initialized in development mode (console logging)'); - } else { - // Production mode - use actual email service - const emailConfig = { - host: process.env.SMTP_HOST, - port: parseInt(process.env.SMTP_PORT) || 587, - secure: process.env.SMTP_SECURE === 'true', - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, - }, - }; - - // Validate required configuration - if (!emailConfig.host || !emailConfig.auth.user || !emailConfig.auth.pass) { - throw new Error('Missing required SMTP configuration. Set SMTP_HOST, SMTP_USER, and SMTP_PASS environment variables.'); - } - - this.transporter = nodemailer.createTransporter(emailConfig); - - // Test the connection - await this.transporter.verify(); - logger.info('Email service initialized with SMTP configuration'); - } - } catch (error) { - logger.error('Failed to initialize email service:', { - error: error.message, - isDevelopment: this.isDevelopment, - }); - throw error; - } - } - - /** - * Send email verification message - * @param {string} to - Recipient email address - * @param {string} username - Player username - * @param {string} verificationToken - Email verification token - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Email sending result - */ - async sendEmailVerification(to, username, verificationToken, correlationId) { - try { - logger.info('Sending email verification', { - correlationId, - to, - username, - }); - - const verificationUrl = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/verify-email?token=${verificationToken}`; - - const subject = 'Verify Your Shattered Void Account'; - - const textContent = ` -Welcome to Shattered Void, ${username}! - -Please verify your email address by clicking the link below: -${verificationUrl} - -This link will expire in 24 hours. - -If you didn't create an account with Shattered Void, you can safely ignore this email. - -The Shattered Void Team - `.trim(); - - const htmlContent = ` - - - - - - - -
-
-

Welcome to Shattered Void

-
-
-

Hello ${username}!

-

Thank you for joining the Shattered Void galaxy. To complete your registration, please verify your email address.

-

- Verify Email Address -

-

Important: This verification link will expire in 24 hours.

-

If the button doesn't work, copy and paste this link into your browser:

-

${verificationUrl}

-
- -
- - - `.trim(); - - const result = await this.transporter.sendMail({ - from: process.env.SMTP_FROM || 'noreply@shatteredvoid.game', - to, - subject, - text: textContent, - html: htmlContent, - }); - - logger.info('Email verification sent successfully', { - correlationId, - to, - messageId: result.messageId, - }); - - return { - success: true, - messageId: result.messageId, - }; - - } catch (error) { - logger.error('Failed to send email verification', { - correlationId, - to, - username, - error: error.message, - stack: error.stack, - }); - - throw new Error('Failed to send verification email'); - } - } - - /** - * Send password reset email - * @param {string} to - Recipient email address - * @param {string} username - Player username - * @param {string} resetToken - Password reset token - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Email sending result - */ - async sendPasswordReset(to, username, resetToken, correlationId) { - try { - logger.info('Sending password reset email', { - correlationId, - to, - username, - }); - - const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/reset-password?token=${resetToken}`; - - const subject = 'Reset Your Shattered Void Password'; - - const textContent = ` -Hello ${username}, - -We received a request to reset your password for your Shattered Void account. - -Click the link below to reset your password: -${resetUrl} - -This link will expire in 1 hour for security reasons. - -If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged. - -The Shattered Void Team - `.trim(); - - const htmlContent = ` - - - - - - - -
-
-

Password Reset Request

-
-
-

Hello ${username},

-

We received a request to reset your password for your Shattered Void account.

-

- Reset Password -

-
- Security Notice: This reset link will expire in 1 hour for your security. -
-

If the button doesn't work, copy and paste this link into your browser:

-

${resetUrl}

-
- -
- - - `.trim(); - - const result = await this.transporter.sendMail({ - from: process.env.SMTP_FROM || 'noreply@shatteredvoid.game', - to, - subject, - text: textContent, - html: htmlContent, - }); - - logger.info('Password reset email sent successfully', { - correlationId, - to, - messageId: result.messageId, - }); - - return { - success: true, - messageId: result.messageId, - }; - - } catch (error) { - logger.error('Failed to send password reset email', { - correlationId, - to, - username, - error: error.message, - stack: error.stack, - }); - - throw new Error('Failed to send password reset email'); - } - } - - /** - * Send security alert email for suspicious activity - * @param {string} to - Recipient email address - * @param {string} username - Player username - * @param {string} alertType - Type of security alert - * @param {Object} details - Alert details - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Email sending result - */ - async sendSecurityAlert(to, username, alertType, details, correlationId) { - try { - logger.info('Sending security alert email', { - correlationId, - to, - username, - alertType, - }); - - const subject = `Security Alert - ${alertType}`; - - const textContent = ` -Security Alert for ${username} - -Alert Type: ${alertType} -Time: ${new Date().toISOString()} - -Details: -${JSON.stringify(details, null, 2)} - -If this activity was performed by you, no action is required. -If you did not perform this activity, please secure your account immediately by changing your password. - -The Shattered Void Security Team - `.trim(); - - const htmlContent = ` - - - - - - - -
-
-

🚨 Security Alert

-
-
-

Hello ${username},

-
- Alert Type: ${alertType}
- Time: ${new Date().toISOString()} -
-

We detected activity on your account that may require your attention.

-
- ${JSON.stringify(details, null, 2)} -
-

If this was you: No action is required.

-

If this was not you: Please secure your account immediately by changing your password.

-
- -
- - - `.trim(); - - const result = await this.transporter.sendMail({ - from: process.env.SMTP_FROM || 'security@shatteredvoid.game', - to, - subject, - text: textContent, - html: htmlContent, - }); - - logger.info('Security alert email sent successfully', { - correlationId, - to, - alertType, - messageId: result.messageId, - }); - - return { - success: true, - messageId: result.messageId, - }; - - } catch (error) { - logger.error('Failed to send security alert email', { - correlationId, - to, - username, - alertType, - error: error.message, - stack: error.stack, - }); - - // Don't throw error for security alerts to avoid blocking user actions - return { - success: false, - error: error.message, - }; - } - } - - /** - * Validate email service health - * @returns {Promise} Service health status - */ - async healthCheck() { - try { - if (this.isDevelopment) { - return true; // Development mode is always healthy - } - - if (!this.transporter) { - return false; - } - - await this.transporter.verify(); - return true; - } catch (error) { - logger.error('Email service health check failed:', { - error: error.message, - }); - return false; - } - } -} - -module.exports = EmailService; \ No newline at end of file diff --git a/src/services/auth/TokenService.js b/src/services/auth/TokenService.js deleted file mode 100644 index 03ff662..0000000 --- a/src/services/auth/TokenService.js +++ /dev/null @@ -1,635 +0,0 @@ -/** - * Token Service - * Handles advanced token management including blacklisting, refresh logic, and token generation - */ - -const { - generatePlayerToken, - generateRefreshToken, - verifyRefreshToken, - verifyPlayerToken -} = require('../../utils/jwt'); -const redis = require('../../utils/redis'); -const logger = require('../../utils/logger'); -const crypto = require('crypto'); -const { v4: uuidv4 } = require('uuid'); - -class TokenService { - constructor() { - this.redisClient = redis; - this.isRedisDisabled = process.env.DISABLE_REDIS === 'true'; - this.TOKEN_BLACKLIST_PREFIX = 'blacklist:token:'; - this.REFRESH_TOKEN_PREFIX = 'refresh:token:'; - this.SECURITY_TOKEN_PREFIX = 'security:token:'; - this.FAILED_ATTEMPTS_PREFIX = 'failed:attempts:'; - this.ACCOUNT_LOCKOUT_PREFIX = 'lockout:account:'; - - // In-memory fallbacks when Redis is disabled - this.memoryStore = { - tokens: new Map(), - refreshTokens: new Map(), - securityTokens: new Map(), - failedAttempts: new Map(), - accountLockouts: new Map(), - blacklistedTokens: new Map(), - }; - } - - // Helper methods for Redis/Memory storage abstraction - async _setWithExpiry(key, value, expirySeconds) { - if (this.isRedisDisabled) { - const store = this._getStoreForKey(key); - const data = { value, expiresAt: Date.now() + (expirySeconds * 1000) }; - store.set(key, data); - return; - } - await this._setWithExpiry(key, expirySeconds, value); - } - - async _get(key) { - if (this.isRedisDisabled) { - const store = this._getStoreForKey(key); - const data = store.get(key); - if (!data) return null; - if (Date.now() > data.expiresAt) { - store.delete(key); - return null; - } - return data.value; - } - return await this._get(key); - } - - async _delete(key) { - if (this.isRedisDisabled) { - const store = this._getStoreForKey(key); - return store.delete(key); - } - return await this._delete(key); - } - - async _incr(key) { - if (this.isRedisDisabled) { - const store = this._getStoreForKey(key); - const current = store.get(key) || { value: 0, expiresAt: Date.now() + (15 * 60 * 1000) }; - current.value++; - store.set(key, current); - return current.value; - } - return await this._incr(key); - } - - async _expire(key, seconds) { - if (this.isRedisDisabled) { - const store = this._getStoreForKey(key); - const data = store.get(key); - if (data) { - data.expiresAt = Date.now() + (seconds * 1000); - store.set(key, data); - } - return; - } - return await this._expire(key, seconds); - } - - _getStoreForKey(key) { - if (key.includes(this.TOKEN_BLACKLIST_PREFIX)) return this.memoryStore.blacklistedTokens; - if (key.includes(this.REFRESH_TOKEN_PREFIX)) return this.memoryStore.refreshTokens; - if (key.includes(this.SECURITY_TOKEN_PREFIX)) return this.memoryStore.securityTokens; - if (key.includes(this.FAILED_ATTEMPTS_PREFIX)) return this.memoryStore.failedAttempts; - if (key.includes(this.ACCOUNT_LOCKOUT_PREFIX)) return this.memoryStore.accountLockouts; - return this.memoryStore.tokens; - } - - async _keys(pattern) { - if (this.isRedisDisabled) { - // Simple pattern matching for memory store - const allKeys = []; - Object.values(this.memoryStore).forEach(store => { - allKeys.push(...store.keys()); - }); - // Basic pattern matching (just prefix matching) - const prefix = pattern.replace('*', ''); - return allKeys.filter(key => key.startsWith(prefix)); - } - return await this.redisClient.keys(pattern); - } - - /** - * Generate secure verification token for email verification - * @param {number} playerId - Player ID - * @param {string} email - Player email - * @param {number} expiresInMinutes - Token expiration in minutes (default 24 hours) - * @returns {Promise} Verification token - */ - async generateEmailVerificationToken(playerId, email, expiresInMinutes = 1440) { - try { - const token = crypto.randomBytes(32).toString('hex'); - const tokenData = { - playerId, - email, - type: 'email_verification', - createdAt: Date.now(), - expiresAt: Date.now() + (expiresInMinutes * 60 * 1000), - }; - - const redisKey = `${this.SECURITY_TOKEN_PREFIX}${token}`; - await this._setWithExpiry(redisKey, JSON.stringify(tokenData), expiresInMinutes * 60); - - logger.info('Email verification token generated', { - playerId, - email, - expiresInMinutes, - tokenPrefix: token.substring(0, 8) + '...', - }); - - return token; - } catch (error) { - logger.error('Failed to generate email verification token', { - playerId, - email, - error: error.message, - }); - throw new Error('Failed to generate verification token'); - } - } - - /** - * Generate secure password reset token - * @param {number} playerId - Player ID - * @param {string} email - Player email - * @param {number} expiresInMinutes - Token expiration in minutes (default 1 hour) - * @returns {Promise} Password reset token - */ - async generatePasswordResetToken(playerId, email, expiresInMinutes = 60) { - try { - const token = crypto.randomBytes(32).toString('hex'); - const tokenData = { - playerId, - email, - type: 'password_reset', - createdAt: Date.now(), - expiresAt: Date.now() + (expiresInMinutes * 60 * 1000), - }; - - const redisKey = `${this.SECURITY_TOKEN_PREFIX}${token}`; - await this._setWithExpiry(redisKey, expiresInMinutes * 60, JSON.stringify(tokenData)); - - logger.info('Password reset token generated', { - playerId, - email, - expiresInMinutes, - tokenPrefix: token.substring(0, 8) + '...', - }); - - return token; - } catch (error) { - logger.error('Failed to generate password reset token', { - playerId, - email, - error: error.message, - }); - throw new Error('Failed to generate reset token'); - } - } - - /** - * Validate and consume security token - * @param {string} token - Security token to validate - * @param {string} expectedType - Expected token type - * @returns {Promise} Token data if valid - */ - async validateSecurityToken(token, expectedType) { - try { - const redisKey = `${this.SECURITY_TOKEN_PREFIX}${token}`; - const tokenDataStr = await this._get(redisKey); - - if (!tokenDataStr) { - logger.warn('Security token not found or expired', { - tokenPrefix: token.substring(0, 8) + '...', - expectedType, - }); - throw new Error('Token not found or expired'); - } - - const tokenData = JSON.parse(tokenDataStr); - - if (tokenData.type !== expectedType) { - logger.warn('Security token type mismatch', { - tokenPrefix: token.substring(0, 8) + '...', - expectedType, - actualType: tokenData.type, - }); - throw new Error('Invalid token type'); - } - - if (Date.now() > tokenData.expiresAt) { - await this._delete(redisKey); - logger.warn('Security token expired', { - tokenPrefix: token.substring(0, 8) + '...', - expiresAt: new Date(tokenData.expiresAt), - }); - throw new Error('Token expired'); - } - - // Consume the token by deleting it - await this._delete(redisKey); - - logger.info('Security token validated and consumed', { - playerId: tokenData.playerId, - type: tokenData.type, - tokenPrefix: token.substring(0, 8) + '...', - }); - - return tokenData; - } catch (error) { - logger.error('Failed to validate security token', { - tokenPrefix: token.substring(0, 8) + '...', - expectedType, - error: error.message, - }); - throw error; - } - } - - /** - * Generate new access and refresh tokens - * @param {Object} playerData - Player data for token payload - * @returns {Promise} New tokens - */ - async generateAuthTokens(playerData) { - try { - const accessToken = generatePlayerToken({ - playerId: playerData.id, - email: playerData.email, - username: playerData.username, - }); - - const refreshToken = generateRefreshToken({ - userId: playerData.id, - type: 'player', - }); - - // Store refresh token in Redis with metadata - const refreshTokenId = uuidv4(); - const refreshTokenData = { - playerId: playerData.id, - email: playerData.email, - tokenId: refreshTokenId, - createdAt: Date.now(), - lastUsed: Date.now(), - userAgent: playerData.userAgent || null, - ipAddress: playerData.ipAddress || null, - }; - - const redisKey = `${this.REFRESH_TOKEN_PREFIX}${refreshTokenId}`; - const expirationSeconds = 7 * 24 * 60 * 60; // 7 days - await this._setWithExpiry(redisKey, JSON.stringify(refreshTokenData), expirationSeconds); - - logger.info('Auth tokens generated', { - playerId: playerData.id, - refreshTokenId, - }); - - return { - accessToken, - refreshToken, - refreshTokenId, - }; - } catch (error) { - logger.error('Failed to generate auth tokens', { - playerId: playerData.id, - error: error.message, - }); - throw new Error('Failed to generate tokens'); - } - } - - /** - * Refresh access token using refresh token - * @param {string} refreshToken - Refresh token - * @param {string} correlationId - Request correlation ID - * @returns {Promise} New access token - */ - async refreshAccessToken(refreshToken, correlationId) { - try { - // Verify refresh token structure - const decoded = verifyRefreshToken(refreshToken); - - // Check if refresh token exists in Redis - const refreshTokenData = await this.getRefreshTokenData(decoded.tokenId); - if (!refreshTokenData) { - throw new Error('Refresh token not found or expired'); - } - - // Check if token belongs to the same user - if (refreshTokenData.playerId !== decoded.userId) { - logger.warn('Refresh token user mismatch', { - correlationId, - tokenUserId: decoded.userId, - storedUserId: refreshTokenData.playerId, - }); - throw new Error('Invalid refresh token'); - } - - // Generate new access token - const accessToken = generatePlayerToken({ - playerId: refreshTokenData.playerId, - email: refreshTokenData.email, - username: refreshTokenData.username || 'Unknown', - }); - - // Update last used timestamp - refreshTokenData.lastUsed = Date.now(); - const redisKey = `${this.REFRESH_TOKEN_PREFIX}${decoded.tokenId}`; - const expirationSeconds = 7 * 24 * 60 * 60; // 7 days - await this._setWithExpiry(redisKey, JSON.stringify(refreshTokenData), expirationSeconds); - - logger.info('Access token refreshed', { - correlationId, - playerId: refreshTokenData.playerId, - refreshTokenId: decoded.tokenId, - }); - - return { - accessToken, - playerId: refreshTokenData.playerId, - email: refreshTokenData.email, - }; - } catch (error) { - logger.error('Failed to refresh access token', { - correlationId, - error: error.message, - }); - throw new Error('Token refresh failed'); - } - } - - /** - * Blacklist a token (for logout or security) - * @param {string} token - Token to blacklist - * @param {string} reason - Reason for blacklisting - * @param {number} expiresInSeconds - How long to keep in blacklist - * @returns {Promise} - */ - async blacklistToken(token, reason = 'logout', expiresInSeconds = 86400) { - try { - const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); - const blacklistData = { - reason, - blacklistedAt: Date.now(), - }; - - const redisKey = `${this.TOKEN_BLACKLIST_PREFIX}${tokenHash}`; - await this._setWithExpiry(redisKey, expiresInSeconds, JSON.stringify(blacklistData)); - - logger.info('Token blacklisted', { - tokenHash: tokenHash.substring(0, 16) + '...', - reason, - expiresInSeconds, - }); - } catch (error) { - logger.error('Failed to blacklist token', { - error: error.message, - reason, - }); - throw error; - } - } - - /** - * Check if a token is blacklisted - * @param {string} token - Token to check - * @returns {Promise} True if blacklisted - */ - async isTokenBlacklisted(token) { - try { - const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); - const redisKey = `${this.TOKEN_BLACKLIST_PREFIX}${tokenHash}`; - const result = await this._get(redisKey); - return result !== null; - } catch (error) { - logger.error('Failed to check token blacklist', { - error: error.message, - }); - return false; // Err on the side of allowing access - } - } - - /** - * Track failed login attempts - * @param {string} identifier - Email or IP address - * @param {number} maxAttempts - Maximum allowed attempts - * @param {number} windowMinutes - Time window in minutes - * @returns {Promise} Attempt tracking data - */ - async trackFailedAttempt(identifier, maxAttempts = 5, windowMinutes = 15) { - try { - const redisKey = `${this.FAILED_ATTEMPTS_PREFIX}${identifier}`; - const currentCount = await this._incr(redisKey); - - if (currentCount === 1) { - // Set expiration on first attempt - await this._expire(redisKey, windowMinutes * 60); - } - - const remainingAttempts = Math.max(0, maxAttempts - currentCount); - const isLocked = currentCount >= maxAttempts; - - if (isLocked && currentCount === maxAttempts) { - // First time hitting the limit, set account lockout - await this.lockAccount(identifier, windowMinutes); - } - - logger.info('Failed login attempt tracked', { - identifier, - attempts: currentCount, - remainingAttempts, - isLocked, - }); - - return { - attempts: currentCount, - remainingAttempts, - isLocked, - lockoutMinutes: isLocked ? windowMinutes : 0, - }; - } catch (error) { - logger.error('Failed to track login attempt', { - identifier, - error: error.message, - }); - throw error; - } - } - - /** - * Check if account is locked - * @param {string} identifier - Email or IP address - * @returns {Promise} Lockout status - */ - async isAccountLocked(identifier) { - try { - const redisKey = `${this.ACCOUNT_LOCKOUT_PREFIX}${identifier}`; - const lockoutData = await this._get(redisKey); - - if (!lockoutData) { - return { isLocked: false }; - } - - const data = JSON.parse(lockoutData); - const isStillLocked = Date.now() < data.expiresAt; - - if (!isStillLocked) { - // Clean up expired lockout - await this._delete(redisKey); - return { isLocked: false }; - } - - return { - isLocked: true, - lockedAt: new Date(data.lockedAt), - expiresAt: new Date(data.expiresAt), - reason: data.reason, - }; - } catch (error) { - logger.error('Failed to check account lockout', { - identifier, - error: error.message, - }); - return { isLocked: false }; // Err on the side of allowing access - } - } - - /** - * Lock account due to security concerns - * @param {string} identifier - Email or IP address - * @param {number} durationMinutes - Lockout duration in minutes - * @param {string} reason - Reason for lockout - * @returns {Promise} - */ - async lockAccount(identifier, durationMinutes = 15, reason = 'Too many failed attempts') { - try { - const lockoutData = { - lockedAt: Date.now(), - expiresAt: Date.now() + (durationMinutes * 60 * 1000), - reason, - }; - - const redisKey = `${this.ACCOUNT_LOCKOUT_PREFIX}${identifier}`; - await this._setWithExpiry(redisKey, durationMinutes * 60, JSON.stringify(lockoutData)); - - logger.warn('Account locked', { - identifier, - durationMinutes, - reason, - }); - } catch (error) { - logger.error('Failed to lock account', { - identifier, - error: error.message, - }); - throw error; - } - } - - /** - * Clear failed attempts (on successful login) - * @param {string} identifier - Email or IP address - * @returns {Promise} - */ - async clearFailedAttempts(identifier) { - try { - const failedKey = `${this.FAILED_ATTEMPTS_PREFIX}${identifier}`; - const lockoutKey = `${this.ACCOUNT_LOCKOUT_PREFIX}${identifier}`; - - await Promise.all([ - this._delete(failedKey), - this._delete(lockoutKey), - ]); - - logger.info('Failed attempts cleared', { identifier }); - } catch (error) { - logger.error('Failed to clear attempts', { - identifier, - error: error.message, - }); - } - } - - /** - * Get refresh token data from Redis - * @param {string} tokenId - Refresh token ID - * @returns {Promise} Token data or null - */ - async getRefreshTokenData(tokenId) { - try { - const redisKey = `${this.REFRESH_TOKEN_PREFIX}${tokenId}`; - const tokenDataStr = await this._get(redisKey); - return tokenDataStr ? JSON.parse(tokenDataStr) : null; - } catch (error) { - logger.error('Failed to get refresh token data', { - tokenId, - error: error.message, - }); - return null; - } - } - - /** - * Revoke refresh token - * @param {string} tokenId - Refresh token ID to revoke - * @returns {Promise} - */ - async revokeRefreshToken(tokenId) { - try { - const redisKey = `${this.REFRESH_TOKEN_PREFIX}${tokenId}`; - await this._delete(redisKey); - - logger.info('Refresh token revoked', { tokenId }); - } catch (error) { - logger.error('Failed to revoke refresh token', { - tokenId, - error: error.message, - }); - throw error; - } - } - - /** - * Revoke all refresh tokens for a user - * @param {number} playerId - Player ID - * @returns {Promise} - */ - async revokeAllUserTokens(playerId) { - try { - const pattern = `${this.REFRESH_TOKEN_PREFIX}*`; - const keys = await this._keys(pattern); - - let revokedCount = 0; - for (const key of keys) { - const tokenDataStr = await this._get(key); - if (tokenDataStr) { - const tokenData = JSON.parse(tokenDataStr); - if (tokenData.playerId === playerId) { - await this._delete(key); - revokedCount++; - } - } - } - - logger.info('All user tokens revoked', { - playerId, - revokedCount, - }); - } catch (error) { - logger.error('Failed to revoke all user tokens', { - playerId, - error: error.message, - }); - throw error; - } - } -} - -module.exports = TokenService; \ No newline at end of file diff --git a/src/services/combat/CombatPluginManager.js b/src/services/combat/CombatPluginManager.js deleted file mode 100644 index c2ec836..0000000 --- a/src/services/combat/CombatPluginManager.js +++ /dev/null @@ -1,743 +0,0 @@ -/** - * Combat Plugin Manager - * Manages combat resolution plugins for different combat types and strategies - */ - -const db = require('../../database/connection'); -const logger = require('../../utils/logger'); -const { ValidationError, ServiceError } = require('../../middleware/error.middleware'); - -class CombatPluginManager { - constructor() { - this.plugins = new Map(); - this.hooks = new Map(); - this.initialized = false; - } - - /** - * Initialize the plugin manager and load active plugins - * @param {string} correlationId - Request correlation ID - * @returns {Promise} - */ - async initialize(correlationId) { - try { - logger.info('Initializing Combat Plugin Manager', { correlationId }); - - // Load active combat plugins from database - const activePlugins = await db('plugins') - .where('plugin_type', 'combat') - .where('is_active', true) - .orderBy('name'); - - for (const pluginData of activePlugins) { - await this.loadPlugin(pluginData, correlationId); - } - - this.initialized = true; - - logger.info('Combat Plugin Manager initialized', { - correlationId, - loadedPlugins: this.plugins.size, - availableHooks: Array.from(this.hooks.keys()), - }); - - } catch (error) { - logger.error('Failed to initialize Combat Plugin Manager', { - correlationId, - error: error.message, - stack: error.stack, - }); - throw new ServiceError('Failed to initialize combat plugin system', error); - } - } - - /** - * Load a combat plugin - * @param {Object} pluginData - Plugin data from database - * @param {string} correlationId - Request correlation ID - * @returns {Promise} - */ - async loadPlugin(pluginData, correlationId) { - try { - logger.info('Loading combat plugin', { - correlationId, - pluginName: pluginData.name, - version: pluginData.version, - }); - - let plugin; - - // Load built-in plugins - switch (pluginData.name) { - case 'instant_combat': - plugin = new InstantCombatPlugin(pluginData.config || {}); - break; - case 'turn_based_combat': - plugin = new TurnBasedCombatPlugin(pluginData.config || {}); - break; - case 'tactical_combat': - plugin = new TacticalCombatPlugin(pluginData.config || {}); - break; - default: - logger.warn('Unknown combat plugin', { - correlationId, - pluginName: pluginData.name, - }); - return; - } - - // Validate plugin interface - this.validatePluginInterface(plugin, pluginData.name); - - // Register plugin - this.plugins.set(pluginData.name, plugin); - - // Register plugin hooks - if (pluginData.hooks && Array.isArray(pluginData.hooks)) { - for (const hook of pluginData.hooks) { - if (!this.hooks.has(hook)) { - this.hooks.set(hook, []); - } - this.hooks.get(hook).push({ - plugin: pluginData.name, - handler: plugin[hook] ? plugin[hook].bind(plugin) : null, - }); - } - } - - logger.info('Combat plugin loaded successfully', { - correlationId, - pluginName: pluginData.name, - hooksRegistered: pluginData.hooks?.length || 0, - }); - - } catch (error) { - logger.error('Failed to load combat plugin', { - correlationId, - pluginName: pluginData.name, - error: error.message, - stack: error.stack, - }); - } - } - - /** - * Resolve combat using appropriate plugin - * @param {Object} battle - Battle data - * @param {Object} forces - Combat forces - * @param {Object} config - Combat configuration - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat result - */ - async resolveCombat(battle, forces, config, correlationId) { - try { - if (!this.initialized) { - await this.initialize(correlationId); - } - - logger.info('Resolving combat with plugin system', { - correlationId, - battleId: battle.id, - combatType: config.combat_type, - }); - - // Determine which plugin to use - const pluginName = this.getPluginForCombatType(config.combat_type); - const plugin = this.plugins.get(pluginName); - - if (!plugin) { - logger.warn('No plugin found for combat type, using fallback', { - correlationId, - combatType: config.combat_type, - requestedPlugin: pluginName, - }); - return await this.fallbackCombatResolver(battle, forces, config, correlationId); - } - - // Execute pre-combat hooks - await this.executeHooks('pre_combat', { battle, forces, config }, correlationId); - - // Resolve combat using plugin - const result = await plugin.resolveCombat(battle, forces, config, correlationId); - - // Execute post-combat hooks - await this.executeHooks('post_combat', { battle, forces, config, result }, correlationId); - - logger.info('Combat resolved successfully', { - correlationId, - battleId: battle.id, - plugin: pluginName, - outcome: result.outcome, - duration: result.duration, - }); - - return result; - - } catch (error) { - logger.error('Combat resolution failed', { - correlationId, - battleId: battle.id, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to resolve combat', error); - } - } - - /** - * Execute hooks for a specific event - * @param {string} hookName - Hook name - * @param {Object} data - Hook data - * @param {string} correlationId - Request correlation ID - * @returns {Promise} - */ - async executeHooks(hookName, data, correlationId) { - const hookHandlers = this.hooks.get(hookName) || []; - - for (const handler of hookHandlers) { - try { - if (handler.handler) { - await handler.handler(data, correlationId); - } - } catch (error) { - logger.error('Hook execution failed', { - correlationId, - hookName, - plugin: handler.plugin, - error: error.message, - }); - } - } - } - - /** - * Get plugin name for combat type - * @param {string} combatType - Combat type - * @returns {string} Plugin name - */ - getPluginForCombatType(combatType) { - const typeMapping = { - instant: 'instant_combat', - turn_based: 'turn_based_combat', - tactical: 'tactical_combat', - real_time: 'tactical_combat', // Use tactical plugin for real-time - }; - - return typeMapping[combatType] || 'instant_combat'; - } - - /** - * Validate plugin interface - * @param {Object} plugin - Plugin instance - * @param {string} pluginName - Plugin name - * @throws {ValidationError} If plugin interface is invalid - */ - validatePluginInterface(plugin, pluginName) { - const requiredMethods = ['resolveCombat']; - - for (const method of requiredMethods) { - if (typeof plugin[method] !== 'function') { - throw new ValidationError(`Plugin ${pluginName} missing required method: ${method}`); - } - } - } - - /** - * Fallback combat resolver - * @param {Object} battle - Battle data - * @param {Object} forces - Combat forces - * @param {Object} config - Combat configuration - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat result - */ - async fallbackCombatResolver(battle, forces, config, correlationId) { - logger.info('Using fallback combat resolver', { correlationId, battleId: battle.id }); - - const fallbackPlugin = new InstantCombatPlugin({}); - return await fallbackPlugin.resolveCombat(battle, forces, config, correlationId); - } - - /** - * Register a new plugin dynamically - * @param {string} name - Plugin name - * @param {Object} plugin - Plugin instance - * @param {Array} hooks - Hook names - * @returns {void} - */ - registerPlugin(name, plugin, hooks = []) { - this.validatePluginInterface(plugin, name); - this.plugins.set(name, plugin); - - // Register hooks - for (const hook of hooks) { - if (!this.hooks.has(hook)) { - this.hooks.set(hook, []); - } - this.hooks.get(hook).push({ - plugin: name, - handler: plugin[hook] ? plugin[hook].bind(plugin) : null, - }); - } - - logger.info('Plugin registered dynamically', { - pluginName: name, - hooksRegistered: hooks.length, - }); - } -} - -/** - * Base Combat Plugin Class - * All combat plugins should extend this class - */ -class BaseCombatPlugin { - constructor(config = {}) { - this.config = config; - } - - /** - * Resolve combat - must be implemented by subclasses - * @param {Object} battle - Battle data - * @param {Object} forces - Combat forces - * @param {Object} config - Combat configuration - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat result - */ - async resolveCombat(battle, forces, config, correlationId) { - throw new Error('resolveCombat method must be implemented by subclass'); - } - - /** - * Calculate base combat rating for a force - * @param {Object} force - Force data (fleet or colony) - * @returns {number} Combat rating - */ - calculateCombatRating(force) { - if (force.fleet) { - return force.fleet.total_combat_rating || 0; - } else if (force.colony) { - return force.colony.total_defense_rating || 0; - } - return 0; - } - - /** - * Apply random variance to a value - * @param {number} baseValue - Base value - * @param {number} variance - Variance percentage (0-1) - * @returns {number} Value with variance applied - */ - applyVariance(baseValue, variance = 0.1) { - const factor = 1 + (Math.random() - 0.5) * variance * 2; - return baseValue * factor; - } - - /** - * Generate combat log entry - * @param {number} round - Round number - * @param {string} eventType - Event type - * @param {string} description - Event description - * @param {Object} data - Additional event data - * @returns {Object} Combat log entry - */ - createLogEntry(round, eventType, description, data = {}) { - return { - round, - event: eventType, - description, - timestamp: new Date(), - ...data, - }; - } -} - -/** - * Instant Combat Plugin - * Resolves combat immediately with simple calculations - */ -class InstantCombatPlugin extends BaseCombatPlugin { - async resolveCombat(battle, forces, config, correlationId) { - logger.info('Resolving instant combat', { correlationId, battleId: battle.id }); - - const attackerRating = this.calculateCombatRating(forces.attacker); - const defenderRating = this.calculateCombatRating(forces.defender); - - // Apply variance - const effectiveAttackerRating = this.applyVariance(attackerRating, this.config.damage_variance || 0.1); - const effectiveDefenderRating = this.applyVariance(defenderRating, this.config.damage_variance || 0.1); - - const totalRating = effectiveAttackerRating + effectiveDefenderRating; - const attackerWinChance = totalRating > 0 ? effectiveAttackerRating / totalRating : 0.5; - - const attackerWins = Math.random() < attackerWinChance; - const outcome = attackerWins ? 'attacker_victory' : 'defender_victory'; - - // Calculate casualties - const casualties = this.calculateInstantCasualties(forces, attackerWins); - - // Generate combat log - const combatLog = [ - this.createLogEntry(1, 'combat_start', 'Combat initiated', { - attacker_strength: effectiveAttackerRating, - defender_strength: effectiveDefenderRating, - win_chance: attackerWinChance, - }), - this.createLogEntry(1, 'combat_resolution', `${outcome.replace('_', ' ')}`, { - winner: attackerWins ? 'attacker' : 'defender', - }), - ]; - - const experienceGained = Math.floor((attackerRating + defenderRating) / 100) * (this.config.experience_gain || 1.0); - - return { - outcome, - casualties, - experience_gained: experienceGained, - combat_log: combatLog, - duration: Math.floor(Math.random() * 60) + 30, // 30-90 seconds - final_forces: this.calculateFinalForces(forces, casualties), - loot: this.calculateLoot(forces, attackerWins), - }; - } - - calculateInstantCasualties(forces, attackerWins) { - const casualties = { - attacker: { ships: {}, total_ships: 0 }, - defender: { ships: {}, total_ships: 0, buildings: {} }, - }; - - // Winner loses 5-25%, loser loses 30-70% - const winnerLossRate = 0.05 + Math.random() * 0.2; - const loserLossRate = 0.3 + Math.random() * 0.4; - - const attackerLossRate = attackerWins ? winnerLossRate : loserLossRate; - const defenderLossRate = attackerWins ? loserLossRate : winnerLossRate; - - // Apply casualties to attacker fleet - if (forces.attacker.fleet && forces.attacker.fleet.ships) { - forces.attacker.fleet.ships.forEach(ship => { - const losses = Math.floor(ship.quantity * attackerLossRate); - if (losses > 0) { - casualties.attacker.ships[ship.design_name] = losses; - casualties.attacker.total_ships += losses; - } - }); - } - - // Apply casualties to defender - if (forces.defender.fleet && forces.defender.fleet.ships) { - forces.defender.fleet.ships.forEach(ship => { - const losses = Math.floor(ship.quantity * defenderLossRate); - if (losses > 0) { - casualties.defender.ships[ship.design_name] = losses; - casualties.defender.total_ships += losses; - } - }); - } - - // Apply building damage if colony and attacker wins - if (forces.defender.colony && attackerWins && forces.defender.colony.defense_buildings) { - const buildingDamageRate = 0.1 + Math.random() * 0.2; - forces.defender.colony.defense_buildings.forEach(building => { - const damage = Math.floor(building.health_percentage * buildingDamageRate); - if (damage > 0) { - casualties.defender.buildings[building.building_name] = damage; - } - }); - } - - return casualties; - } - - calculateFinalForces(forces, casualties) { - const finalForces = JSON.parse(JSON.stringify(forces)); - - // Apply ship losses - ['attacker', 'defender'].forEach(side => { - if (finalForces[side].fleet && finalForces[side].fleet.ships) { - finalForces[side].fleet.ships.forEach(ship => { - const losses = casualties[side].ships[ship.design_name] || 0; - ship.quantity = Math.max(0, ship.quantity - losses); - }); - } - }); - - // Apply building damage - if (finalForces.defender.colony && finalForces.defender.colony.defense_buildings) { - finalForces.defender.colony.defense_buildings.forEach(building => { - const damage = casualties.defender.buildings[building.building_name] || 0; - building.health_percentage = Math.max(0, building.health_percentage - damage); - }); - } - - return finalForces; - } - - calculateLoot(forces, attackerWins) { - if (!attackerWins) return {}; - - const loot = {}; - const baseAmount = Math.floor(Math.random() * 500) + 100; - - loot.scrap = baseAmount; - loot.energy = Math.floor(baseAmount * 0.6); - - if (forces.defender.colony) { - loot.data_cores = Math.floor(Math.random() * 5) + 1; - if (Math.random() < 0.15) { - loot.rare_elements = Math.floor(Math.random() * 3) + 1; - } - } - - return loot; - } -} - -/** - * Turn-Based Combat Plugin - * Resolves combat in discrete rounds with detailed calculations - */ -class TurnBasedCombatPlugin extends BaseCombatPlugin { - async resolveCombat(battle, forces, config, correlationId) { - logger.info('Resolving turn-based combat', { correlationId, battleId: battle.id }); - - const maxRounds = this.config.max_rounds || 10; - const combatLog = []; - let round = 1; - - // Initialize combat state - const combatState = this.initializeCombatState(forces); - - combatLog.push(this.createLogEntry(0, 'combat_start', 'Turn-based combat initiated', { - attacker_ships: combatState.attacker.totalShips, - defender_ships: combatState.defender.totalShips, - max_rounds: maxRounds, - })); - - // Combat rounds - while (round <= maxRounds && !this.isCombatOver(combatState)) { - const roundResult = await this.processRound(combatState, round, correlationId); - combatLog.push(...roundResult.log); - round++; - } - - // Determine outcome - const outcome = this.determineTurnBasedOutcome(combatState); - const casualties = this.calculateTurnBasedCasualties(forces, combatState); - - combatLog.push(this.createLogEntry(round - 1, 'combat_end', `Combat ended: ${outcome}`, { - total_rounds: round - 1, - attacker_survivors: combatState.attacker.totalShips, - defender_survivors: combatState.defender.totalShips, - })); - - return { - outcome, - casualties, - experience_gained: Math.floor((round - 1) * 50), - combat_log: combatLog, - duration: (round - 1) * 30, // 30 seconds per round - final_forces: this.calculateFinalForces(forces, casualties), - loot: this.calculateLoot(forces, outcome === 'attacker_victory'), - }; - } - - initializeCombatState(forces) { - const state = { - attacker: { totalShips: 0, effectiveStrength: 0 }, - defender: { totalShips: 0, effectiveStrength: 0 }, - }; - - // Calculate initial state - if (forces.attacker.fleet && forces.attacker.fleet.ships) { - state.attacker.totalShips = forces.attacker.fleet.ships.reduce((sum, ship) => sum + ship.quantity, 0); - state.attacker.effectiveStrength = forces.attacker.fleet.total_combat_rating || 0; - } - - if (forces.defender.fleet && forces.defender.fleet.ships) { - state.defender.totalShips = forces.defender.fleet.ships.reduce((sum, ship) => sum + ship.quantity, 0); - state.defender.effectiveStrength = forces.defender.fleet.total_combat_rating || 0; - } else if (forces.defender.colony) { - state.defender.totalShips = 1; // Represent colony as single entity - state.defender.effectiveStrength = forces.defender.colony.total_defense_rating || 0; - } - - return state; - } - - async processRound(combatState, round, correlationId) { - const log = []; - - // Attacker attacks defender - const attackerDamage = this.applyVariance(combatState.attacker.effectiveStrength * 0.1, 0.2); - const defenderLosses = Math.floor(attackerDamage / 100); - - combatState.defender.totalShips = Math.max(0, combatState.defender.totalShips - defenderLosses); - combatState.defender.effectiveStrength *= (combatState.defender.totalShips / (combatState.defender.totalShips + defenderLosses)); - - log.push(this.createLogEntry(round, 'attack', 'Attacker strikes', { - damage: attackerDamage, - defender_losses: defenderLosses, - defender_remaining: combatState.defender.totalShips, - })); - - // Defender counterattacks if still alive - if (combatState.defender.totalShips > 0) { - const defenderDamage = this.applyVariance(combatState.defender.effectiveStrength * 0.1, 0.2); - const attackerLosses = Math.floor(defenderDamage / 100); - - combatState.attacker.totalShips = Math.max(0, combatState.attacker.totalShips - attackerLosses); - combatState.attacker.effectiveStrength *= (combatState.attacker.totalShips / (combatState.attacker.totalShips + attackerLosses)); - - log.push(this.createLogEntry(round, 'counterattack', 'Defender counterattacks', { - damage: defenderDamage, - attacker_losses: attackerLosses, - attacker_remaining: combatState.attacker.totalShips, - })); - } - - return { log }; - } - - isCombatOver(combatState) { - return combatState.attacker.totalShips <= 0 || combatState.defender.totalShips <= 0; - } - - determineTurnBasedOutcome(combatState) { - if (combatState.attacker.totalShips <= 0 && combatState.defender.totalShips <= 0) { - return 'draw'; - } else if (combatState.attacker.totalShips > 0) { - return 'attacker_victory'; - } else { - return 'defender_victory'; - } - } - - calculateTurnBasedCasualties(forces, combatState) { - // Calculate casualties based on ships remaining vs initial - const casualties = { - attacker: { ships: {}, total_ships: 0 }, - defender: { ships: {}, total_ships: 0, buildings: {} }, - }; - - // Calculate attacker casualties - if (forces.attacker.fleet && forces.attacker.fleet.ships) { - const initialShips = forces.attacker.fleet.ships.reduce((sum, ship) => sum + ship.quantity, 0); - const remainingRatio = combatState.attacker.totalShips / initialShips; - - forces.attacker.fleet.ships.forEach(ship => { - const remaining = Math.floor(ship.quantity * remainingRatio); - const losses = ship.quantity - remaining; - if (losses > 0) { - casualties.attacker.ships[ship.design_name] = losses; - casualties.attacker.total_ships += losses; - } - }); - } - - // Calculate defender casualties - if (forces.defender.fleet && forces.defender.fleet.ships) { - const initialShips = forces.defender.fleet.ships.reduce((sum, ship) => sum + ship.quantity, 0); - const remainingRatio = combatState.defender.totalShips / initialShips; - - forces.defender.fleet.ships.forEach(ship => { - const remaining = Math.floor(ship.quantity * remainingRatio); - const losses = ship.quantity - remaining; - if (losses > 0) { - casualties.defender.ships[ship.design_name] = losses; - casualties.defender.total_ships += losses; - } - }); - } else if (forces.defender.colony && combatState.defender.totalShips <= 0) { - // Colony was destroyed or heavily damaged - if (forces.defender.colony.defense_buildings) { - forces.defender.colony.defense_buildings.forEach(building => { - const damage = Math.floor(building.health_percentage * (0.3 + Math.random() * 0.4)); - casualties.defender.buildings[building.building_name] = damage; - }); - } - } - - return casualties; - } - - calculateFinalForces(forces, casualties) { - const finalForces = JSON.parse(JSON.stringify(forces)); - - // Apply ship casualties - ['attacker', 'defender'].forEach(side => { - if (finalForces[side].fleet && finalForces[side].fleet.ships) { - finalForces[side].fleet.ships.forEach(ship => { - const losses = casualties[side].ships[ship.design_name] || 0; - ship.quantity = Math.max(0, ship.quantity - losses); - }); - } - }); - - // Apply building damage - if (finalForces.defender.colony && finalForces.defender.colony.defense_buildings) { - finalForces.defender.colony.defense_buildings.forEach(building => { - const damage = casualties.defender.buildings[building.building_name] || 0; - building.health_percentage = Math.max(0, building.health_percentage - damage); - }); - } - - return finalForces; - } - - calculateLoot(forces, attackerWins) { - if (!attackerWins) return {}; - - const loot = {}; - const baseAmount = Math.floor(Math.random() * 800) + 200; - - loot.scrap = baseAmount; - loot.energy = Math.floor(baseAmount * 0.7); - - if (forces.defender.colony) { - loot.data_cores = Math.floor(Math.random() * 8) + 2; - if (Math.random() < 0.2) { - loot.rare_elements = Math.floor(Math.random() * 5) + 1; - } - } - - return loot; - } -} - -/** - * Tactical Combat Plugin - * Advanced combat with formations, positioning, and tactical decisions - */ -class TacticalCombatPlugin extends BaseCombatPlugin { - async resolveCombat(battle, forces, config, correlationId) { - logger.info('Resolving tactical combat', { correlationId, battleId: battle.id }); - - // For MVP, use enhanced turn-based system - // Future versions can implement full tactical positioning - const turnBasedPlugin = new TurnBasedCombatPlugin(this.config); - const result = await turnBasedPlugin.resolveCombat(battle, forces, config, correlationId); - - // Add tactical bonuses based on formations and positioning - result.experience_gained *= 1.5; // Tactical combat gives more experience - result.duration *= 1.2; // Takes longer than simple turn-based - - // Enhanced loot for tactical victory - if (result.loot && Object.keys(result.loot).length > 0) { - Object.keys(result.loot).forEach(resource => { - result.loot[resource] = Math.floor(result.loot[resource] * 1.3); - }); - } - - return result; - } -} - -module.exports = { - CombatPluginManager, - BaseCombatPlugin, - InstantCombatPlugin, - TurnBasedCombatPlugin, - TacticalCombatPlugin, -}; diff --git a/src/services/combat/CombatService.js b/src/services/combat/CombatService.js deleted file mode 100644 index 590f210..0000000 --- a/src/services/combat/CombatService.js +++ /dev/null @@ -1,1446 +0,0 @@ -/** - * Combat Service - * Handles all combat-related business logic including fleet battles, colony sieges, and combat resolution - */ - -const db = require('../../database/connection'); -const logger = require('../../utils/logger'); -const { ValidationError, ConflictError, NotFoundError, ServiceError } = require('../../middleware/error.middleware'); - -class CombatService { - constructor(gameEventService = null, combatPluginManager = null) { - this.gameEventService = gameEventService; - this.combatPluginManager = combatPluginManager; - this.activeCombats = new Map(); // Track ongoing combats - } - - /** - * Initiate combat between fleets or fleet vs colony - * @param {Object} combatData - Combat initiation data - * @param {number} combatData.attacker_fleet_id - Attacking fleet ID - * @param {number|null} combatData.defender_fleet_id - Defending fleet ID (null for colony) - * @param {number|null} combatData.defender_colony_id - Defending colony ID (null for fleet) - * @param {string} combatData.location - Combat location coordinates - * @param {string} combatData.combat_type - Type of combat ('instant', 'turn_based', 'real_time') - * @param {number} attackerPlayerId - Attacking player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat initiation result - */ - async initiateCombat(combatData, attackerPlayerId, correlationId) { - try { - const { attacker_fleet_id, defender_fleet_id, defender_colony_id, location, combat_type = 'instant' } = combatData; - - logger.info('Combat initiation started', { - correlationId, - attackerPlayerId, - attacker_fleet_id, - defender_fleet_id, - defender_colony_id, - location, - combat_type, - }); - - // Validate combat data - await this.validateCombatInitiation(combatData, attackerPlayerId, correlationId); - - // Check if any participants are already in combat - const conflictCheck = await this.checkCombatConflicts(attacker_fleet_id, defender_fleet_id, defender_colony_id); - if (conflictCheck.hasConflict) { - throw new ConflictError(`Combat participant already engaged: ${conflictCheck.reason}`); - } - - // Get combat configuration - const combatConfig = await this.getCombatConfiguration(combat_type); - if (!combatConfig) { - throw new ValidationError('Invalid combat type specified'); - } - - // Database transaction for atomic combat creation - const combat = await db.transaction(async (trx) => { - // Create battle record - const [battle] = await trx('battles').insert({ - battle_type: defender_colony_id ? 'fleet_vs_colony' : 'fleet_vs_fleet', - location, - combat_type_id: combatConfig.id, - participants: JSON.stringify({ - attacker_fleet_id, - defender_fleet_id, - defender_colony_id, - attacker_player_id: attackerPlayerId, - }), - status: 'preparing', - battle_data: JSON.stringify({ - combat_phase: 'preparation', - preparation_time: combatConfig.config_data.preparation_time || 30, - }), - combat_configuration_id: combatConfig.id, - tactical_settings: JSON.stringify({}), - spectator_count: 0, - estimated_duration: combatConfig.config_data.estimated_duration || 60, - started_at: new Date(), - created_at: new Date(), - }).returning('*'); - - // Update fleet statuses to 'in_combat' - await trx('fleets') - .whereIn('id', [attacker_fleet_id, defender_fleet_id].filter(Boolean)) - .update({ - fleet_status: 'in_combat', - last_updated: new Date(), - }); - - // Update colony status if defending colony - if (defender_colony_id) { - await trx('colonies') - .where('id', defender_colony_id) - .update({ - under_siege: true, - last_updated: new Date(), - }); - } - - // Add to combat queue for processing - await trx('combat_queue').insert({ - battle_id: battle.id, - queue_status: 'pending', - priority: combatConfig.config_data.priority || 100, - scheduled_at: new Date(), - processing_metadata: JSON.stringify({ - combat_type, - auto_resolve: combatConfig.config_data.auto_resolve || true, - }), - }); - - logger.info('Combat initiated successfully', { - correlationId, - battleId: battle.id, - attackerPlayerId, - combatType: combat_type, - }); - - return battle; - }); - - // Add to active combats tracking - this.activeCombats.set(combat.id, { - battleId: combat.id, - status: 'preparing', - participants: JSON.parse(combat.participants), - startedAt: combat.started_at, - }); - - // Emit WebSocket event for combat initiation - if (this.gameEventService) { - await this.gameEventService.emitCombatInitiated(combat, correlationId); - } - - // Auto-resolve combat if configured - if (combatConfig.config_data.auto_resolve) { - setTimeout(() => { - this.processCombat(combat.id, correlationId).catch(error => { - logger.error('Auto-resolve combat failed', { - correlationId, - battleId: combat.id, - error: error.message, - }); - }); - }, (combatConfig.config_data.preparation_time || 5) * 1000); - } - - return { - battleId: combat.id, - status: combat.status, - estimatedDuration: combat.estimated_duration, - preparationTime: combatConfig.config_data.preparation_time || 30, - }; - - } catch (error) { - logger.error('Combat initiation failed', { - correlationId, - attackerPlayerId, - combatData, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError || error instanceof ConflictError || error instanceof NotFoundError) { - throw error; - } - throw new ServiceError('Failed to initiate combat', error); - } - } - - /** - * Process combat resolution using configured plugin - * @param {number} battleId - Battle ID to process - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat result - */ - async processCombat(battleId, correlationId) { - try { - logger.info('Processing combat', { - correlationId, - battleId, - }); - - // Get battle data - const battle = await this.getBattleById(battleId); - if (!battle) { - throw new NotFoundError('Battle not found'); - } - - if (battle.status !== 'preparing' && battle.status !== 'active') { - throw new ConflictError('Battle is not in a processable state'); - } - - // Get combat forces - const combatForces = await this.getCombatForces(battle, correlationId); - - // Get combat configuration - const combatConfig = await this.getCombatConfiguration(null, battle.combat_configuration_id); - - // Database transaction for combat resolution - const result = await db.transaction(async (trx) => { - // Update battle status to active - await trx('battles') - .where('id', battleId) - .update({ - status: 'active', - battle_data: JSON.stringify({ - combat_phase: 'resolution', - processing_started: new Date(), - }), - }); - - // Resolve combat using plugin system - const combatResult = await this.resolveCombat(battle, combatForces, combatConfig, trx, correlationId); - - // Create combat encounter record - const [encounter] = await trx('combat_encounters').insert({ - battle_id: battleId, - attacker_fleet_id: combatForces.attacker.fleet?.id || null, - defender_fleet_id: combatForces.defender.fleet?.id || null, - defender_colony_id: combatForces.defender.colony?.id || null, - encounter_type: battle.battle_type, - location: battle.location, - initial_forces: JSON.stringify(combatForces.initial), - final_forces: JSON.stringify(combatResult.final_forces), - casualties: JSON.stringify(combatResult.casualties), - combat_log: JSON.stringify(combatResult.combat_log), - experience_gained: combatResult.experience_gained || 0, - loot_awarded: JSON.stringify(combatResult.loot || {}), - outcome: combatResult.outcome, - duration_seconds: combatResult.duration || 60, - started_at: battle.started_at, - completed_at: new Date(), - }).returning('*'); - - // Update battle with final result - await trx('battles') - .where('id', battleId) - .update({ - status: 'completed', - result: JSON.stringify(combatResult), - completed_at: new Date(), - }); - - // Apply combat results to fleets and colonies - await this.applyCombatResults(combatResult, combatForces, trx, correlationId); - - // Update combat statistics - await this.updateCombatStatistics(combatResult, combatForces, trx, correlationId); - - // Update combat queue - await trx('combat_queue') - .where('battle_id', battleId) - .update({ - queue_status: 'completed', - completed_at: new Date(), - }); - - logger.info('Combat processed successfully', { - correlationId, - battleId, - encounterId: encounter.id, - outcome: combatResult.outcome, - duration: combatResult.duration, - }); - - return { - battleId, - encounterId: encounter.id, - outcome: combatResult.outcome, - casualties: combatResult.casualties, - experience: combatResult.experience_gained, - loot: combatResult.loot, - duration: combatResult.duration, - }; - }); - - // Remove from active combats - this.activeCombats.delete(battleId); - - // Emit WebSocket event for combat completion - if (this.gameEventService) { - await this.gameEventService.emitCombatCompleted(result, correlationId); - } - - return result; - - } catch (error) { - logger.error('Combat processing failed', { - correlationId, - battleId, - error: error.message, - stack: error.stack, - }); - - // Update combat queue with error - await db('combat_queue') - .where('battle_id', battleId) - .update({ - queue_status: 'failed', - error_message: error.message, - completed_at: new Date(), - }) - .catch(dbError => { - logger.error('Failed to update combat queue error', { - correlationId, - battleId, - dbError: dbError.message, - }); - }); - - if (error instanceof ValidationError || error instanceof ConflictError || error instanceof NotFoundError) { - throw error; - } - throw new ServiceError('Failed to process combat', error); - } - } - - /** - * Get combat history for a player - * @param {number} playerId - Player ID - * @param {Object} options - Query options - * @param {number} options.limit - Result limit - * @param {number} options.offset - Result offset - * @param {string} options.outcome - Filter by outcome - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat history - */ - async getCombatHistory(playerId, options = {}, correlationId) { - try { - const { limit = 50, offset = 0, outcome } = options; - - logger.info('Fetching combat history', { - correlationId, - playerId, - limit, - offset, - outcome, - }); - - let query = db('combat_encounters') - .select([ - 'combat_encounters.*', - 'battles.battle_type', - 'battles.location', - 'attacker_fleet.name as attacker_fleet_name', - 'defender_fleet.name as defender_fleet_name', - 'defender_colony.name as defender_colony_name', - ]) - .join('battles', 'combat_encounters.battle_id', 'battles.id') - .leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id') - .leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id') - .leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id') - .where(function () { - this.where('attacker_fleet.player_id', playerId) - .orWhere('defender_fleet.player_id', playerId) - .orWhere('defender_colony.player_id', playerId); - }) - .orderBy('combat_encounters.completed_at', 'desc') - .limit(limit) - .offset(offset); - - if (outcome) { - query = query.where('combat_encounters.outcome', outcome); - } - - const combats = await query; - - // Get total count for pagination - let countQuery = db('combat_encounters') - .join('battles', 'combat_encounters.battle_id', 'battles.id') - .leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id') - .leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id') - .leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id') - .where(function () { - this.where('attacker_fleet.player_id', playerId) - .orWhere('defender_fleet.player_id', playerId) - .orWhere('defender_colony.player_id', playerId); - }) - .count('* as total'); - - if (outcome) { - countQuery = countQuery.where('combat_encounters.outcome', outcome); - } - - const [{ total }] = await countQuery; - - logger.info('Combat history retrieved', { - correlationId, - playerId, - combatCount: combats.length, - totalCombats: parseInt(total), - }); - - return { - combats, - pagination: { - total: parseInt(total), - limit, - offset, - hasMore: (offset + limit) < parseInt(total), - }, - }; - - } catch (error) { - logger.error('Failed to fetch combat history', { - correlationId, - playerId, - options, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve combat history', error); - } - } - - /** - * Get active combats for a player - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Active combats - */ - async getActiveCombats(playerId, correlationId) { - try { - logger.info('Fetching active combats', { - correlationId, - playerId, - }); - - const activeCombats = await db('battles') - .select([ - 'battles.*', - 'attacker_fleet.name as attacker_fleet_name', - 'defender_fleet.name as defender_fleet_name', - 'defender_colony.name as defender_colony_name', - ]) - .leftJoin('fleets as attacker_fleet', - db.raw('JSON_EXTRACT(battles.participants, \'$.attacker_fleet_id\')'), - 'attacker_fleet.id') - .leftJoin('fleets as defender_fleet', - db.raw('JSON_EXTRACT(battles.participants, \'$.defender_fleet_id\')'), - 'defender_fleet.id') - .leftJoin('colonies as defender_colony', - db.raw('JSON_EXTRACT(battles.participants, \'$.defender_colony_id\')'), - 'defender_colony.id') - .where(function () { - this.where('attacker_fleet.player_id', playerId) - .orWhere('defender_fleet.player_id', playerId) - .orWhere('defender_colony.player_id', playerId); - }) - .whereIn('battles.status', ['preparing', 'active']) - .orderBy('battles.started_at', 'desc'); - - logger.info('Active combats retrieved', { - correlationId, - playerId, - activeCount: activeCombats.length, - }); - - return activeCombats; - - } catch (error) { - logger.error('Failed to fetch active combats', { - correlationId, - playerId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve active combats', error); - } - } - - // Helper methods - - /** - * Validate combat initiation data - * @param {Object} combatData - Combat data to validate - * @param {number} attackerPlayerId - Attacking player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} - */ - async validateCombatInitiation(combatData, attackerPlayerId, correlationId) { - const { attacker_fleet_id, defender_fleet_id, defender_colony_id, location } = combatData; - - // Validate attacker fleet - const attackerFleet = await db('fleets') - .where('id', attacker_fleet_id) - .where('player_id', attackerPlayerId) - .where('fleet_status', 'idle') - .first(); - - if (!attackerFleet) { - throw new ValidationError('Invalid attacker fleet or fleet not available for combat'); - } - - // Validate location matches fleet location - if (attackerFleet.current_location !== location) { - throw new ValidationError('Fleet must be at the specified location to initiate combat'); - } - - // Validate defender (either fleet or colony) - if (!defender_fleet_id && !defender_colony_id) { - throw new ValidationError('Must specify either defender fleet or defender colony'); - } - - if (defender_fleet_id && defender_colony_id) { - throw new ValidationError('Cannot specify both defender fleet and defender colony'); - } - - if (defender_fleet_id) { - const defenderFleet = await db('fleets') - .where('id', defender_fleet_id) - .where('current_location', location) - .first(); - - if (!defenderFleet) { - throw new ValidationError('Defender fleet not found at specified location'); - } - - // Check if attacking own fleet - if (defenderFleet.player_id === attackerPlayerId) { - throw new ValidationError('Cannot attack your own fleet'); - } - } - - if (defender_colony_id) { - const defenderColony = await db('colonies') - .where('id', defender_colony_id) - .where('coordinates', location) - .first(); - - if (!defenderColony) { - throw new ValidationError('Defender colony not found at specified location'); - } - - // Check if attacking own colony - if (defenderColony.player_id === attackerPlayerId) { - throw new ValidationError('Cannot attack your own colony'); - } - } - } - - /** - * Check for combat conflicts with existing battles - * @param {number} attackerFleetId - Attacker fleet ID - * @param {number|null} defenderFleetId - Defender fleet ID - * @param {number|null} defenderColonyId - Defender colony ID - * @returns {Promise} Conflict check result - */ - async checkCombatConflicts(attackerFleetId, defenderFleetId, defenderColonyId) { - // Check if any fleet is already in combat - const fleetsInCombat = await db('fleets') - .whereIn('id', [attackerFleetId, defenderFleetId].filter(Boolean)) - .where('fleet_status', 'in_combat'); - - if (fleetsInCombat.length > 0) { - return { - hasConflict: true, - reason: `Fleet ${fleetsInCombat[0].id} is already in combat`, - }; - } - - // Check if colony is under siege - if (defenderColonyId) { - const colonyUnderSiege = await db('colonies') - .where('id', defenderColonyId) - .where('under_siege', true) - .first(); - - if (colonyUnderSiege) { - return { - hasConflict: true, - reason: `Colony ${defenderColonyId} is already under siege`, - }; - } - } - - return { hasConflict: false }; - } - - /** - * Get combat configuration by type or ID - * @param {string|null} combatType - Combat type name - * @param {number|null} configId - Configuration ID - * @returns {Promise} Combat configuration - */ - async getCombatConfiguration(combatType = null, configId = null) { - try { - let query = db('combat_configurations').where('is_active', true); - - if (configId) { - query = query.where('id', configId); - } else if (combatType) { - query = query.where('combat_type', combatType); - } else { - // Default to instant combat - query = query.where('combat_type', 'instant'); - } - - return await query.first(); - } catch (error) { - logger.error('Failed to get combat configuration', { error: error.message }); - return null; - } - } - - /** - * Get battle by ID with full details - * @param {number} battleId - Battle ID - * @returns {Promise} Battle data - */ - async getBattleById(battleId) { - try { - return await db('battles') - .where('id', battleId) - .first(); - } catch (error) { - logger.error('Failed to get battle', { battleId, error: error.message }); - return null; - } - } - - /** - * Get combat forces for battle resolution - * @param {Object} battle - Battle data - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat forces - */ - async getCombatForces(battle, correlationId) { - const participants = JSON.parse(battle.participants); - const forces = { - attacker: {}, - defender: {}, - initial: {}, - }; - - // Get attacker fleet - if (participants.attacker_fleet_id) { - forces.attacker.fleet = await this.getFleetCombatData(participants.attacker_fleet_id); - } - - // Get defender fleet or colony - if (participants.defender_fleet_id) { - forces.defender.fleet = await this.getFleetCombatData(participants.defender_fleet_id); - } else if (participants.defender_colony_id) { - forces.defender.colony = await this.getColonyCombatData(participants.defender_colony_id); - } - - // Save initial forces snapshot - forces.initial = JSON.parse(JSON.stringify({ attacker: forces.attacker, defender: forces.defender })); - - return forces; - } - - /** - * Get fleet combat data including ships and stats - * @param {number} fleetId - Fleet ID - * @returns {Promise} Fleet combat data - */ - async getFleetCombatData(fleetId) { - const fleet = await db('fleets') - .where('id', fleetId) - .first(); - - if (!fleet) return null; - - const ships = await db('fleet_ships') - .select([ - 'fleet_ships.*', - 'ship_designs.name as design_name', - 'ship_designs.ship_class', - 'ship_designs.hull_points', - 'ship_designs.shield_points', - 'ship_designs.armor_points', - 'ship_designs.attack_power', - 'ship_designs.attack_speed', - 'ship_designs.movement_speed', - 'ship_designs.special_abilities', - ]) - .join('ship_designs', 'fleet_ships.ship_design_id', 'ship_designs.id') - .where('fleet_ships.fleet_id', fleetId); - - // Get combat experience for ships - const experience = await db('ship_combat_experience') - .where('fleet_id', fleetId); - - const experienceMap = {}; - experience.forEach(exp => { - experienceMap[exp.ship_design_id] = exp; - }); - - // Calculate fleet combat rating - let totalCombatRating = 0; - const shipDetails = ships.map(ship => { - const exp = experienceMap[ship.ship_design_id] || {}; - const veterancyBonus = (exp.veterancy_level || 1) * 0.1; - const effectiveAttack = ship.attack_power * (1 + veterancyBonus); - const effectiveHp = (ship.hull_points + ship.shield_points + ship.armor_points) * (1 + veterancyBonus); - - const shipRating = (effectiveAttack * ship.attack_speed + effectiveHp) * ship.quantity; - totalCombatRating += shipRating; - - return { - ...ship, - experience: exp, - effective_attack: effectiveAttack, - effective_hp: effectiveHp, - combat_rating: shipRating, - }; - }); - - return { - ...fleet, - ships: shipDetails, - total_combat_rating: totalCombatRating, - }; - } - - /** - * Get colony combat data including defenses - * @param {number} colonyId - Colony ID - * @returns {Promise} Colony combat data - */ - async getColonyCombatData(colonyId) { - const colony = await db('colonies') - .where('id', colonyId) - .first(); - - if (!colony) return null; - - // Get defensive buildings - const defenseBuildings = await db('colony_buildings') - .select([ - 'colony_buildings.*', - 'building_types.name as building_name', - 'building_types.special_effects', - ]) - .join('building_types', 'colony_buildings.building_type_id', 'building_types.id') - .where('colony_buildings.colony_id', colonyId) - .where('building_types.category', 'military'); - - // Calculate total defense rating - let totalDefenseRating = colony.defense_rating || 0; - defenseBuildings.forEach(building => { - const effects = building.special_effects || {}; - const defenseBonus = effects.defense_rating || 0; - totalDefenseRating += defenseBonus * building.level * (building.health_percentage / 100); - }); - - return { - ...colony, - defense_buildings: defenseBuildings, - total_defense_rating: totalDefenseRating, - effective_hp: totalDefenseRating * 10 + (colony.shield_strength || 0), - }; - } - - /** - * Resolve combat using plugin system - * @param {Object} battle - Battle data - * @param {Object} forces - Combat forces - * @param {Object} config - Combat configuration - * @param {Object} trx - Database transaction - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat result - */ - async resolveCombat(battle, forces, config, trx, correlationId) { - if (this.combatPluginManager) { - return await this.combatPluginManager.resolveCombat(battle, forces, config, correlationId); - } - - // Fallback instant combat resolver - return await this.instantCombatResolver(battle, forces, config, correlationId); - } - - /** - * Instant combat resolver (fallback implementation) - * @param {Object} battle - Battle data - * @param {Object} forces - Combat forces - * @param {Object} config - Combat configuration - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat result - */ - async instantCombatResolver(battle, forces, config, correlationId) { - const attackerRating = forces.attacker.fleet?.total_combat_rating || 0; - const defenderRating = forces.defender.fleet?.total_combat_rating || forces.defender.colony?.total_defense_rating || 0; - - const totalRating = attackerRating + defenderRating; - const attackerWinChance = totalRating > 0 ? attackerRating / totalRating : 0.5; - - // Add some randomness - const randomFactor = 0.1; // 10% randomness - const roll = Math.random(); - const adjustedChance = attackerWinChance + (Math.random() - 0.5) * randomFactor; - - const attackerWins = roll < adjustedChance; - const outcome = attackerWins ? 'attacker_victory' : 'defender_victory'; - - // Calculate casualties - const casualties = this.calculateCasualties(forces, attackerWins, correlationId); - - // Calculate experience gain - const experienceGained = Math.floor((attackerRating + defenderRating) / 100); - - // Generate combat log - const combatLog = [ - { - round: 1, - event: 'combat_start', - description: 'Combat initiated', - attacker_strength: attackerRating, - defender_strength: defenderRating, - }, - { - round: 1, - event: 'combat_resolution', - description: `${outcome.replace('_', ' ')}`, - winner: attackerWins ? 'attacker' : 'defender', - }, - ]; - - return { - outcome, - casualties, - experience_gained: experienceGained, - combat_log: combatLog, - duration: Math.floor(Math.random() * 120) + 30, // 30-150 seconds - final_forces: this.calculateFinalForces(forces, casualties), - loot: this.calculateLoot(forces, attackerWins, correlationId), - }; - } - - /** - * Calculate combat casualties - * @param {Object} forces - Combat forces - * @param {boolean} attackerWins - Whether attacker won - * @param {string} correlationId - Request correlation ID - * @returns {Object} Casualty breakdown - */ - calculateCasualties(forces, attackerWins, correlationId) { - const casualties = { - attacker: { ships: {}, total_ships: 0 }, - defender: { ships: {}, total_ships: 0, buildings: {} }, - }; - - // Calculate ship losses (winner loses 10-30%, loser loses 40-80%) - const attackerLossRate = attackerWins ? (0.1 + Math.random() * 0.2) : (0.4 + Math.random() * 0.4); - const defenderLossRate = attackerWins ? (0.4 + Math.random() * 0.4) : (0.1 + Math.random() * 0.2); - - // Attacker casualties - if (forces.attacker.fleet && forces.attacker.fleet.ships) { - forces.attacker.fleet.ships.forEach(ship => { - const losses = Math.floor(ship.quantity * attackerLossRate); - if (losses > 0) { - casualties.attacker.ships[ship.design_name] = losses; - casualties.attacker.total_ships += losses; - } - }); - } - - // Defender casualties - if (forces.defender.fleet && forces.defender.fleet.ships) { - forces.defender.fleet.ships.forEach(ship => { - const losses = Math.floor(ship.quantity * defenderLossRate); - if (losses > 0) { - casualties.defender.ships[ship.design_name] = losses; - casualties.defender.total_ships += losses; - } - }); - } - - // Colony building damage - if (forces.defender.colony && attackerWins) { - const buildingDamageRate = 0.1 + Math.random() * 0.3; // 10-40% damage - if (forces.defender.colony.defense_buildings) { - forces.defender.colony.defense_buildings.forEach(building => { - const damage = Math.floor(building.health_percentage * buildingDamageRate); - if (damage > 0) { - casualties.defender.buildings[building.building_name] = damage; - } - }); - } - } - - return casualties; - } - - /** - * Calculate final forces after combat - * @param {Object} forces - Initial forces - * @param {Object} casualties - Combat casualties - * @returns {Object} Final forces - */ - calculateFinalForces(forces, casualties) { - const finalForces = JSON.parse(JSON.stringify(forces)); - - // Apply attacker casualties - if (finalForces.attacker.fleet && finalForces.attacker.fleet.ships) { - finalForces.attacker.fleet.ships.forEach(ship => { - const losses = casualties.attacker.ships[ship.design_name] || 0; - ship.quantity = Math.max(0, ship.quantity - losses); - }); - } - - // Apply defender casualties - if (finalForces.defender.fleet && finalForces.defender.fleet.ships) { - finalForces.defender.fleet.ships.forEach(ship => { - const losses = casualties.defender.ships[ship.design_name] || 0; - ship.quantity = Math.max(0, ship.quantity - losses); - }); - } - - // Apply building damage - if (finalForces.defender.colony && finalForces.defender.colony.defense_buildings) { - finalForces.defender.colony.defense_buildings.forEach(building => { - const damage = casualties.defender.buildings[building.building_name] || 0; - building.health_percentage = Math.max(0, building.health_percentage - damage); - }); - } - - return finalForces; - } - - /** - * Calculate loot from combat - * @param {Object} forces - Combat forces - * @param {boolean} attackerWins - Whether attacker won - * @param {string} correlationId - Request correlation ID - * @returns {Object} Loot awarded - */ - calculateLoot(forces, attackerWins, correlationId) { - if (!attackerWins) return {}; - - const loot = {}; - - // Base loot from combat - const baseLoot = Math.floor(Math.random() * 1000) + 100; - loot.scrap = baseLoot; - loot.energy = Math.floor(baseLoot * 0.5); - - // Additional loot from colony raids - if (forces.defender.colony) { - loot.data_cores = Math.floor(Math.random() * 10) + 1; - if (Math.random() < 0.1) { // 10% chance for rare elements - loot.rare_elements = Math.floor(Math.random() * 5) + 1; - } - } - - return loot; - } - - /** - * Apply combat results to fleets and colonies - * @param {Object} result - Combat result - * @param {Object} forces - Combat forces - * @param {Object} trx - Database transaction - * @param {string} correlationId - Request correlation ID - * @returns {Promise} - */ - async applyCombatResults(result, forces, trx, correlationId) { - // Update fleet ship quantities - for (const side of ['attacker', 'defender']) { - const fleet = forces[side].fleet; - if (fleet && result.casualties[side].ships) { - for (const ship of fleet.ships) { - const losses = result.casualties[side].ships[ship.design_name] || 0; - if (losses > 0) { - const newQuantity = Math.max(0, ship.quantity - losses); - await trx('fleet_ships') - .where('id', ship.id) - .update({ - quantity: newQuantity, - health_percentage: newQuantity > 0 ? - Math.max(20, ship.health_percentage - Math.floor(Math.random() * 30)) : - 0, - }); - } - } - - // Update fleet status and statistics - const isDestroyed = result.final_forces[side].fleet.ships.every(ship => ship.quantity === 0); - const newStatus = isDestroyed ? 'destroyed' : 'idle'; - - await trx('fleets') - .where('id', fleet.id) - .update({ - fleet_status: newStatus, - last_combat: new Date(), - last_updated: new Date(), - combat_victories: side === result.outcome.split('_')[0] ? - db.raw('combat_victories + 1') : db.raw('combat_victories'), - combat_defeats: side !== result.outcome.split('_')[0] ? - db.raw('combat_defeats + 1') : db.raw('combat_defeats'), - }); - } - } - - // Update colony if involved - if (forces.defender.colony) { - const colony = forces.defender.colony; - const buildingDamage = result.casualties.defender.buildings || {}; - - // Apply building damage - for (const building of colony.defense_buildings) { - const damage = buildingDamage[building.building_name] || 0; - if (damage > 0) { - await trx('colony_buildings') - .where('id', building.id) - .update({ - health_percentage: Math.max(0, building.health_percentage - damage), - }); - } - } - - // Update colony status - await trx('colonies') - .where('id', colony.id) - .update({ - under_siege: false, - last_attacked: new Date(), - successful_defenses: result.outcome === 'defender_victory' ? - db.raw('successful_defenses + 1') : db.raw('successful_defenses'), - times_captured: result.outcome === 'attacker_victory' ? - db.raw('times_captured + 1') : db.raw('times_captured'), - }); - } - - // Award loot to winner - if (result.loot && Object.keys(result.loot).length > 0) { - const winnerId = result.outcome === 'attacker_victory' ? - forces.attacker.fleet.player_id : - (forces.defender.fleet ? forces.defender.fleet.player_id : forces.defender.colony.player_id); - - for (const [resourceName, amount] of Object.entries(result.loot)) { - await trx('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', winnerId) - .where('resource_types.name', resourceName) - .increment('player_resources.amount', amount) - .update('player_resources.last_updated', new Date()); - } - } - } - - /** - * Update combat statistics for participants - * @param {Object} result - Combat result - * @param {Object} forces - Combat forces - * @param {Object} trx - Database transaction - * @param {string} correlationId - Request correlation ID - * @returns {Promise} - */ - async updateCombatStatistics(result, forces, trx, correlationId) { - const participants = []; - - // Collect all participants - if (forces.attacker.fleet) { - participants.push({ - playerId: forces.attacker.fleet.player_id, - side: 'attacker', - isWinner: result.outcome === 'attacker_victory', - }); - } - - if (forces.defender.fleet) { - participants.push({ - playerId: forces.defender.fleet.player_id, - side: 'defender', - isWinner: result.outcome === 'defender_victory', - }); - } else if (forces.defender.colony) { - participants.push({ - playerId: forces.defender.colony.player_id, - side: 'defender', - isWinner: result.outcome === 'defender_victory', - }); - } - - // Update statistics for each participant - for (const participant of participants) { - const stats = { - battles_initiated: participant.side === 'attacker' ? 1 : 0, - battles_won: participant.isWinner ? 1 : 0, - battles_lost: participant.isWinner ? 0 : 1, - ships_lost: result.casualties[participant.side].total_ships || 0, - ships_destroyed: result.casualties[participant.side === 'attacker' ? 'defender' : 'attacker'].total_ships || 0, - total_experience_gained: participant.isWinner ? result.experience_gained : 0, - last_battle: new Date(), - }; - - await trx('combat_statistics') - .where('player_id', participant.playerId) - .update({ - battles_initiated: db.raw(`battles_initiated + ${stats.battles_initiated}`), - battles_won: db.raw(`battles_won + ${stats.battles_won}`), - battles_lost: db.raw(`battles_lost + ${stats.battles_lost}`), - ships_lost: db.raw(`ships_lost + ${stats.ships_lost}`), - ships_destroyed: db.raw(`ships_destroyed + ${stats.ships_destroyed}`), - total_experience_gained: db.raw(`total_experience_gained + ${stats.total_experience_gained}`), - last_battle: stats.last_battle, - updated_at: new Date(), - }); - - // Insert if no existing record - const existingStats = await trx('combat_statistics') - .where('player_id', participant.playerId) - .first(); - - if (!existingStats) { - await trx('combat_statistics').insert({ - player_id: participant.playerId, - ...stats, - created_at: new Date(), - updated_at: new Date(), - }); - } - } - } - - /** - * Get combat encounter details - * @param {number} encounterId - Encounter ID - * @param {number} playerId - Player ID for access control - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat encounter details - */ - async getCombatEncounter(encounterId, playerId, correlationId) { - try { - logger.info('Fetching combat encounter', { - correlationId, - encounterId, - playerId, - }); - - const encounter = await db('combat_encounters') - .select([ - 'combat_encounters.*', - 'battles.battle_type', - 'battles.location', - 'attacker_fleet.name as attacker_fleet_name', - 'attacker_fleet.player_id as attacker_player_id', - 'defender_fleet.name as defender_fleet_name', - 'defender_fleet.player_id as defender_player_id', - 'defender_colony.name as defender_colony_name', - 'defender_colony.player_id as defender_colony_player_id', - ]) - .join('battles', 'combat_encounters.battle_id', 'battles.id') - .leftJoin('fleets as attacker_fleet', 'combat_encounters.attacker_fleet_id', 'attacker_fleet.id') - .leftJoin('fleets as defender_fleet', 'combat_encounters.defender_fleet_id', 'defender_fleet.id') - .leftJoin('colonies as defender_colony', 'combat_encounters.defender_colony_id', 'defender_colony.id') - .where('combat_encounters.id', encounterId) - .first(); - - if (!encounter) { - return null; - } - - // Check if player has access to this encounter - const hasAccess = encounter.attacker_player_id === playerId || - encounter.defender_player_id === playerId || - encounter.defender_colony_player_id === playerId; - - if (!hasAccess) { - return null; - } - - // Get combat logs - const combatLogs = await db('combat_logs') - .where('encounter_id', encounterId) - .orderBy('round_number') - .orderBy('timestamp'); - - logger.info('Combat encounter retrieved', { - correlationId, - encounterId, - playerId, - logCount: combatLogs.length, - }); - - return { - ...encounter, - combat_logs: combatLogs, - }; - - } catch (error) { - logger.error('Failed to fetch combat encounter', { - correlationId, - encounterId, - playerId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve combat encounter', error); - } - } - - /** - * Get combat statistics for a player - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat statistics - */ - async getCombatStatistics(playerId, correlationId) { - try { - logger.info('Fetching combat statistics', { - correlationId, - playerId, - }); - - let statistics = await db('combat_statistics') - .where('player_id', playerId) - .first(); - - // Create default statistics if none exist - if (!statistics) { - statistics = { - player_id: playerId, - battles_initiated: 0, - battles_won: 0, - battles_lost: 0, - ships_lost: 0, - ships_destroyed: 0, - total_damage_dealt: 0, - total_damage_received: 0, - total_experience_gained: 0, - resources_looted: {}, - last_battle: null, - }; - } - - // Calculate derived statistics - const totalBattles = statistics.battles_won + statistics.battles_lost; - const winRate = totalBattles > 0 ? (statistics.battles_won / totalBattles * 100).toFixed(1) : 0; - const killDeathRatio = statistics.ships_lost > 0 ? - (statistics.ships_destroyed / statistics.ships_lost).toFixed(2) : - statistics.ships_destroyed; - - logger.info('Combat statistics retrieved', { - correlationId, - playerId, - totalBattles, - winRate, - }); - - return { - ...statistics, - derived_stats: { - total_battles: totalBattles, - win_rate_percentage: parseFloat(winRate), - kill_death_ratio: parseFloat(killDeathRatio), - average_experience_per_battle: totalBattles > 0 ? - (statistics.total_experience_gained / totalBattles).toFixed(1) : 0, - }, - }; - - } catch (error) { - logger.error('Failed to fetch combat statistics', { - correlationId, - playerId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve combat statistics', error); - } - } - - /** - * Update fleet position for tactical combat - * @param {number} fleetId - Fleet ID - * @param {Object} positionData - Position and formation data - * @param {number} playerId - Player ID for authorization - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Updated position data - */ - async updateFleetPosition(fleetId, positionData, playerId, correlationId) { - try { - const { position_x, position_y, position_z, formation, tactical_settings } = positionData; - - logger.info('Updating fleet position', { - correlationId, - fleetId, - playerId, - formation, - }); - - // Verify fleet ownership - const fleet = await db('fleets') - .where('id', fleetId) - .where('player_id', playerId) - .first(); - - if (!fleet) { - throw new NotFoundError('Fleet not found or access denied'); - } - - // Validate formation type - const validFormations = ['standard', 'defensive', 'aggressive', 'flanking', 'escort']; - if (formation && !validFormations.includes(formation)) { - throw new ValidationError('Invalid formation type'); - } - - // Update fleet position - const result = await db.transaction(async (trx) => { - // Insert or update fleet position - const existingPosition = await trx('fleet_positions') - .where('fleet_id', fleetId) - .first(); - - const positionUpdateData = { - fleet_id: fleetId, - location: fleet.current_location, - position_x: position_x || 0, - position_y: position_y || 0, - position_z: position_z || 0, - formation: formation || 'standard', - tactical_settings: JSON.stringify(tactical_settings || {}), - last_updated: new Date(), - }; - - if (existingPosition) { - await trx('fleet_positions') - .where('fleet_id', fleetId) - .update(positionUpdateData); - } else { - await trx('fleet_positions').insert(positionUpdateData); - } - - return positionUpdateData; - }); - - logger.info('Fleet position updated', { - correlationId, - fleetId, - formation: result.formation, - }); - - return result; - - } catch (error) { - logger.error('Failed to update fleet position', { - correlationId, - fleetId, - playerId, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError || error instanceof NotFoundError) { - throw error; - } - throw new ServiceError('Failed to update fleet position', error); - } - } - - /** - * Get available combat types and configurations - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Available combat types - */ - async getAvailableCombatTypes(correlationId) { - try { - logger.info('Fetching available combat types', { correlationId }); - - const combatTypes = await db('combat_configurations') - .where('is_active', true) - .orderBy('combat_type') - .orderBy('config_name'); - - logger.info('Combat types retrieved', { - correlationId, - count: combatTypes.length, - }); - - return combatTypes; - - } catch (error) { - logger.error('Failed to fetch combat types', { - correlationId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve combat types', error); - } - } - - /** - * Get combat queue status - * @param {Object} options - Query options - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Combat queue entries - */ - async getCombatQueue(options = {}, correlationId) { - try { - const { status, limit = 50 } = options; - - logger.info('Fetching combat queue', { - correlationId, - status, - limit, - }); - - let query = db('combat_queue') - .select([ - 'combat_queue.*', - 'battles.battle_type', - 'battles.location', - 'battles.status as battle_status', - ]) - .join('battles', 'combat_queue.battle_id', 'battles.id') - .orderBy('combat_queue.priority', 'desc') - .orderBy('combat_queue.scheduled_at', 'asc') - .limit(limit); - - if (status) { - query = query.where('combat_queue.queue_status', status); - } - - const queue = await query; - - logger.info('Combat queue retrieved', { - correlationId, - count: queue.length, - }); - - return queue; - - } catch (error) { - logger.error('Failed to fetch combat queue', { - correlationId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve combat queue', error); - } - } -} - -module.exports = CombatService; diff --git a/src/services/fleet/FleetService.js b/src/services/fleet/FleetService.js deleted file mode 100644 index ba5cdaa..0000000 --- a/src/services/fleet/FleetService.js +++ /dev/null @@ -1,875 +0,0 @@ -/** - * Fleet Service - * Handles fleet creation, management, movement, and ship construction - */ - -const logger = require('../../utils/logger'); -const db = require('../../database/connection'); -const ShipDesignService = require('./ShipDesignService'); - -class FleetService { - constructor(gameEventService = null, shipDesignService = null) { - this.gameEventService = gameEventService; - this.shipDesignService = shipDesignService || new ShipDesignService(gameEventService); - } - - /** - * Get all fleets for a player - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Player fleets - */ - async getPlayerFleets(playerId, correlationId) { - try { - logger.info('Getting fleets for player', { - correlationId, - playerId - }); - - const fleets = await db('fleets') - .select([ - 'fleets.*', - db.raw('COUNT(fleet_ships.id) as ship_count'), - db.raw('SUM(fleet_ships.quantity) as total_ships') - ]) - .leftJoin('fleet_ships', 'fleets.id', 'fleet_ships.fleet_id') - .where('fleets.player_id', playerId) - .groupBy('fleets.id') - .orderBy('fleets.created_at', 'desc'); - - // Get detailed ship composition for each fleet - for (const fleet of fleets) { - const ships = await db('fleet_ships') - .select([ - 'fleet_ships.*', - 'ship_designs.name as design_name', - 'ship_designs.ship_class', - 'ship_designs.stats' - ]) - .leftJoin('ship_designs', 'fleet_ships.ship_design_id', 'ship_designs.id') - .where('fleet_ships.fleet_id', fleet.id) - .orderBy('ship_designs.ship_class'); - - fleet.ships = ships.map(ship => ({ - ...ship, - stats: typeof ship.stats === 'string' ? JSON.parse(ship.stats) : ship.stats - })); - - // Convert counts to integers - fleet.ship_count = parseInt(fleet.ship_count) || 0; - fleet.total_ships = parseInt(fleet.total_ships) || 0; - } - - logger.debug('Player fleets retrieved', { - correlationId, - playerId, - fleetCount: fleets.length, - totalFleets: fleets.reduce((sum, fleet) => sum + fleet.total_ships, 0) - }); - - return fleets; - - } catch (error) { - logger.error('Failed to get player fleets', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Get fleet details by ID - * @param {number} fleetId - Fleet ID - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Fleet details - */ - async getFleetDetails(fleetId, playerId, correlationId) { - try { - logger.info('Getting fleet details', { - correlationId, - playerId, - fleetId - }); - - const fleet = await db('fleets') - .select('*') - .where('id', fleetId) - .where('player_id', playerId) - .first(); - - if (!fleet) { - const error = new Error('Fleet not found'); - error.statusCode = 404; - throw error; - } - - // Get fleet ships with design details - const ships = await db('fleet_ships') - .select([ - 'fleet_ships.*', - 'ship_designs.name as design_name', - 'ship_designs.ship_class', - 'ship_designs.hull_type', - 'ship_designs.stats', - 'ship_designs.components' - ]) - .leftJoin('ship_designs', 'fleet_ships.ship_design_id', 'ship_designs.id') - .where('fleet_ships.fleet_id', fleetId) - .orderBy('ship_designs.ship_class'); - - fleet.ships = ships.map(ship => ({ - ...ship, - stats: typeof ship.stats === 'string' ? JSON.parse(ship.stats) : ship.stats, - components: typeof ship.components === 'string' ? JSON.parse(ship.components) : ship.components - })); - - // Calculate fleet statistics - fleet.combat_stats = this.calculateFleetCombatStats(fleet.ships); - fleet.total_ships = fleet.ships.reduce((sum, ship) => sum + ship.quantity, 0); - - logger.debug('Fleet details retrieved', { - correlationId, - playerId, - fleetId, - totalShips: fleet.total_ships, - fleetStatus: fleet.fleet_status - }); - - return fleet; - - } catch (error) { - logger.error('Failed to get fleet details', { - correlationId, - playerId, - fleetId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Create a new fleet - * @param {number} playerId - Player ID - * @param {Object} fleetData - Fleet creation data - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Created fleet - */ - async createFleet(playerId, fleetData, correlationId) { - try { - logger.info('Creating fleet for player', { - correlationId, - playerId, - fleetName: fleetData.name - }); - - const { name, location, ship_composition } = fleetData; - - // Validate location is a player colony - const colony = await db('colonies') - .select('id', 'coordinates', 'name') - .where('player_id', playerId) - .where('coordinates', location) - .first(); - - if (!colony) { - const error = new Error('Fleet must be created at a player colony'); - error.statusCode = 400; - throw error; - } - - // Validate and calculate ship construction - let totalCost = {}; - let totalBuildTime = 0; - const validatedShips = []; - - for (const shipRequest of ship_composition) { - const validation = await this.shipDesignService.validateShipConstruction( - playerId, - shipRequest.design_id, - shipRequest.quantity, - correlationId - ); - - if (!validation.valid) { - const error = new Error(`Cannot build ships: ${validation.error}`); - error.statusCode = 400; - error.details = validation; - throw error; - } - - validatedShips.push({ - design_id: shipRequest.design_id, - quantity: shipRequest.quantity, - design: validation.design, - cost: validation.total_cost, - build_time: validation.total_build_time - }); - - // Accumulate costs - Object.entries(validation.total_cost).forEach(([resource, cost]) => { - totalCost[resource] = (totalCost[resource] || 0) + cost; - }); - - totalBuildTime = Math.max(totalBuildTime, validation.total_build_time); - } - - // Create fleet in transaction - const result = await db.transaction(async (trx) => { - // Deduct resources - for (const [resourceName, cost] of Object.entries(totalCost)) { - const updated = await trx('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId) - .where('resource_types.name', resourceName) - .decrement('amount', cost); - - if (updated === 0) { - throw new Error(`Failed to deduct ${resourceName}: insufficient resources`); - } - } - - // Create fleet - const [fleet] = await trx('fleets') - .insert({ - player_id: playerId, - name: name, - current_location: location, - fleet_status: 'constructing', - created_at: new Date() - }) - .returning('*'); - - // Add ships to fleet - for (const ship of validatedShips) { - await trx('fleet_ships') - .insert({ - fleet_id: fleet.id, - ship_design_id: ship.design_id, - quantity: ship.quantity, - health_percentage: 100, - experience: 0 - }); - } - - return { - fleet: fleet, - ships: validatedShips, - total_cost: totalCost, - construction_time: totalBuildTime - }; - }); - - // Schedule fleet completion (in a real implementation, this would be handled by game tick) - // For now, we'll mark it as constructing and let game tick handle completion - - // Emit WebSocket events - if (this.gameEventService) { - // Emit resource deduction - const resourceChanges = {}; - Object.entries(totalCost).forEach(([resourceName, cost]) => { - resourceChanges[resourceName] = -cost; - }); - - this.gameEventService.emitResourcesUpdated( - playerId, - resourceChanges, - 'fleet_construction_started', - correlationId - ); - - // Emit fleet creation event - this.gameEventService.emitFleetCreated( - playerId, - result.fleet, - correlationId - ); - } - - logger.info('Fleet created successfully', { - correlationId, - playerId, - fleetId: result.fleet.id, - fleetName: name, - totalShips: validatedShips.reduce((sum, ship) => sum + ship.quantity, 0), - constructionTime: totalBuildTime - }); - - return result; - - } catch (error) { - logger.error('Failed to create fleet', { - correlationId, - playerId, - fleetName: fleetData.name, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Move fleet to a new location - * @param {number} fleetId - Fleet ID - * @param {number} playerId - Player ID - * @param {string} destination - Destination coordinates - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Movement result - */ - async moveFleet(fleetId, playerId, destination, correlationId) { - try { - logger.info('Moving fleet', { - correlationId, - playerId, - fleetId, - destination - }); - - // Get fleet details - const fleet = await db('fleets') - .select('*') - .where('id', fleetId) - .where('player_id', playerId) - .first(); - - if (!fleet) { - const error = new Error('Fleet not found'); - error.statusCode = 404; - throw error; - } - - if (fleet.fleet_status !== 'idle') { - const error = new Error(`Fleet is currently ${fleet.fleet_status} and cannot move`); - error.statusCode = 400; - throw error; - } - - if (fleet.current_location === destination) { - const error = new Error('Fleet is already at the destination'); - error.statusCode = 400; - throw error; - } - - // Calculate travel time based on fleet composition and distance - const travelTime = await this.calculateTravelTime( - fleet.current_location, - destination, - fleetId, - correlationId - ); - - const arrivalTime = new Date(Date.now() + travelTime * 60 * 1000); // Convert minutes to milliseconds - - // Update fleet status and destination - await db('fleets') - .where('id', fleetId) - .update({ - destination: destination, - fleet_status: 'moving', - movement_started: new Date(), - arrival_time: arrivalTime, - last_updated: new Date() - }); - - const result = { - fleet_id: fleetId, - from: fleet.current_location, - to: destination, - travel_time_minutes: travelTime, - arrival_time: arrivalTime.toISOString(), - status: 'moving' - }; - - // Emit WebSocket event - if (this.gameEventService) { - this.gameEventService.emitFleetMovementStarted( - playerId, - result, - correlationId - ); - } - - logger.info('Fleet movement started', { - correlationId, - playerId, - fleetId, - from: fleet.current_location, - to: destination, - travelTime: travelTime, - arrivalTime: arrivalTime.toISOString() - }); - - return result; - - } catch (error) { - logger.error('Failed to move fleet', { - correlationId, - playerId, - fleetId, - destination, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Disband a fleet - * @param {number} fleetId - Fleet ID - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Disbanding result - */ - async disbandFleet(fleetId, playerId, correlationId) { - try { - logger.info('Disbanding fleet', { - correlationId, - playerId, - fleetId - }); - - const fleet = await db('fleets') - .select('*') - .where('id', fleetId) - .where('player_id', playerId) - .first(); - - if (!fleet) { - const error = new Error('Fleet not found'); - error.statusCode = 404; - throw error; - } - - if (fleet.fleet_status === 'in_combat') { - const error = new Error('Cannot disband fleet while in combat'); - error.statusCode = 400; - throw error; - } - - // Get fleet ships for salvage calculation - const ships = await db('fleet_ships') - .select([ - 'fleet_ships.*', - 'ship_designs.cost', - 'ship_designs.name' - ]) - .leftJoin('ship_designs', 'fleet_ships.ship_design_id', 'ship_designs.id') - .where('fleet_ships.fleet_id', fleetId); - - // Calculate salvage value (50% of original cost) - const salvageResources = {}; - - ships.forEach(ship => { - const designCost = typeof ship.cost === 'string' ? JSON.parse(ship.cost) : ship.cost; - const salvageMultiplier = 0.5 * (ship.health_percentage / 100); - - Object.entries(designCost).forEach(([resource, cost]) => { - const salvageAmount = Math.floor(cost * ship.quantity * salvageMultiplier); - salvageResources[resource] = (salvageResources[resource] || 0) + salvageAmount; - }); - }); - - // Disband fleet in transaction - const result = await db.transaction(async (trx) => { - // Delete fleet ships - await trx('fleet_ships') - .where('fleet_id', fleetId) - .delete(); - - // Delete fleet - await trx('fleets') - .where('id', fleetId) - .delete(); - - // Add salvage resources - for (const [resourceName, amount] of Object.entries(salvageResources)) { - if (amount > 0) { - await trx('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId) - .where('resource_types.name', resourceName) - .increment('amount', amount); - } - } - - return { - fleet_id: fleetId, - fleet_name: fleet.name, - ships_disbanded: ships.length, - salvage_recovered: salvageResources - }; - }); - - // Emit WebSocket events - if (this.gameEventService) { - // Emit resource gain from salvage - if (Object.values(salvageResources).some(amount => amount > 0)) { - this.gameEventService.emitResourcesUpdated( - playerId, - salvageResources, - 'fleet_disbanded_salvage', - correlationId - ); - } - - // Emit fleet disbanded event - this.gameEventService.emitFleetDisbanded( - playerId, - result, - correlationId - ); - } - - logger.info('Fleet disbanded successfully', { - correlationId, - playerId, - fleetId, - fleetName: fleet.name, - salvageRecovered: Object.values(salvageResources).reduce((sum, amount) => sum + amount, 0) - }); - - return result; - - } catch (error) { - logger.error('Failed to disband fleet', { - correlationId, - playerId, - fleetId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Process fleet movements (called from game tick) - * @param {number} playerId - Player ID - * @param {number} tickNumber - Current tick number - * @returns {Promise} Array of completed movements - */ - async processFleetMovements(playerId, tickNumber) { - try { - const now = new Date(); - - // Get fleets that should arrive - const arrivingFleets = await db('fleets') - .select('*') - .where('player_id', playerId) - .where('fleet_status', 'moving') - .where('arrival_time', '<=', now); - - const completedMovements = []; - - for (const fleet of arrivingFleets) { - // Update fleet location and status - await db('fleets') - .where('id', fleet.id) - .update({ - current_location: fleet.destination, - destination: null, - fleet_status: 'idle', - movement_started: null, - arrival_time: null, - last_updated: now - }); - - const movementResult = { - fleet_id: fleet.id, - fleet_name: fleet.name, - arrived_at: fleet.destination, - arrival_time: now.toISOString() - }; - - completedMovements.push(movementResult); - - // Emit WebSocket event - if (this.gameEventService) { - this.gameEventService.emitFleetMovementCompleted( - playerId, - movementResult, - `tick-${tickNumber}-fleet-arrival` - ); - } - - logger.info('Fleet movement completed', { - playerId, - tickNumber, - fleetId: fleet.id, - fleetName: fleet.name, - destination: fleet.destination - }); - } - - return completedMovements; - - } catch (error) { - logger.error('Failed to process fleet movements', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Calculate travel time between locations - * @param {string} from - Source coordinates - * @param {string} to - Destination coordinates - * @param {number} fleetId - Fleet ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Travel time in minutes - */ - async calculateTravelTime(from, to, fleetId, correlationId) { - try { - // Get fleet composition to calculate speed - const ships = await db('fleet_ships') - .select([ - 'fleet_ships.quantity', - 'ship_designs.stats' - ]) - .join('ship_designs', 'fleet_ships.ship_design_id', 'ship_designs.id') - .where('fleet_ships.fleet_id', fleetId); - - if (ships.length === 0) { - return 60; // Default 1 hour for empty fleets - } - - // Calculate fleet speed (limited by slowest ship) - let minSpeed = Infinity; - ships.forEach(ship => { - const stats = typeof ship.stats === 'string' ? JSON.parse(ship.stats) : ship.stats; - const speed = stats.speed || 1; - minSpeed = Math.min(minSpeed, speed); - }); - - // Parse coordinates to calculate distance - const distance = this.calculateDistance(from, to); - - // Travel time calculation: base time modified by distance and speed - const baseTime = 30; // 30 minutes base travel time - const speedModifier = 10 / Math.max(1, minSpeed); // Higher speed = lower time - const distanceModifier = Math.max(0.5, distance); // Distance affects time - - const travelTime = Math.ceil(baseTime * speedModifier * distanceModifier); - - logger.debug('Travel time calculated', { - correlationId, - fleetId, - from, - to, - distance, - fleetSpeed: minSpeed, - travelTime - }); - - return travelTime; - - } catch (error) { - logger.error('Failed to calculate travel time', { - correlationId, - fleetId, - from, - to, - error: error.message, - stack: error.stack - }); - return 60; // Default fallback - } - } - - /** - * Calculate distance between coordinates - * @param {string} from - Source coordinates (e.g., "A3-91-X") - * @param {string} to - Destination coordinates - * @returns {number} Distance modifier - */ - calculateDistance(from, to) { - try { - // Parse coordinate format: "A3-91-X" - const parseCoords = (coords) => { - const parts = coords.split('-'); - if (parts.length !== 3) return null; - - const sector = parts[0]; // A3 - const system = parseInt(parts[1]); // 91 - const planet = parts[2]; // X - - return { sector, system, planet }; - }; - - const fromCoords = parseCoords(from); - const toCoords = parseCoords(to); - - if (!fromCoords || !toCoords) { - return 1.0; // Default distance if parsing fails - } - - // Same planet - if (from === to) { - return 0.1; - } - - // Same system - if (fromCoords.sector === toCoords.sector && fromCoords.system === toCoords.system) { - return 0.5; - } - - // Same sector - if (fromCoords.sector === toCoords.sector) { - const systemDiff = Math.abs(fromCoords.system - toCoords.system); - return 1.0 + (systemDiff * 0.1); - } - - // Different sectors - return 2.0; - - } catch (error) { - logger.warn('Failed to calculate coordinate distance', { from, to, error: error.message }); - return 1.0; // Default distance - } - } - - /** - * Calculate fleet combat statistics - * @param {Array} ships - Fleet ships array - * @returns {Object} Combined combat stats - */ - calculateFleetCombatStats(ships) { - const stats = { - total_hp: 0, - total_attack: 0, - total_defense: 0, - average_speed: 0, - total_ships: 0 - }; - - if (!ships || ships.length === 0) { - return stats; - } - - let totalSpeed = 0; - let shipCount = 0; - - ships.forEach(ship => { - const shipStats = ship.stats || {}; - const quantity = ship.quantity || 1; - const healthMod = (ship.health_percentage || 100) / 100; - - stats.total_hp += (shipStats.hp || 0) * quantity * healthMod; - stats.total_attack += (shipStats.attack || 0) * quantity * healthMod; - stats.total_defense += (shipStats.defense || 0) * quantity; - - totalSpeed += (shipStats.speed || 0) * quantity; - shipCount += quantity; - }); - - stats.total_ships = shipCount; - stats.average_speed = shipCount > 0 ? totalSpeed / shipCount : 0; - - return stats; - } - - /** - * Process fleet construction for game tick - * @param {number} playerId - Player ID - * @param {number} tickNumber - Current tick number - * @returns {Promise} Array of completed construction - */ - async processFleetConstruction(playerId, tickNumber) { - try { - const now = new Date(); - - // Get fleets under construction that should be completed - const completingFleets = await db('fleets') - .select('*') - .where('player_id', playerId) - .where('fleet_status', 'under_construction') - .where('construction_completion_time', '<=', now); - - const completedConstruction = []; - - for (const fleet of completingFleets) { - // Complete fleet construction - await db('fleets') - .where('id', fleet.id) - .update({ - fleet_status: 'idle', - construction_completion_time: null, - last_updated: now - }); - - const constructionResult = { - fleet_id: fleet.id, - fleet_name: fleet.name, - location: fleet.current_location, - ships_constructed: await this.getFleetShipCount(fleet.id), - construction_time: fleet.construction_time - }; - - completedConstruction.push(constructionResult); - - // Emit WebSocket event - if (this.gameEventService) { - this.gameEventService.emitFleetConstructionCompleted( - playerId, - constructionResult, - `tick-${tickNumber}-fleet-construction` - ); - } - - logger.info('Fleet construction completed', { - playerId, - tickNumber, - fleetId: fleet.id, - fleetName: fleet.name, - location: fleet.current_location - }); - } - - return completedConstruction; - - } catch (error) { - logger.error('Failed to process fleet construction', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Get total ship count for a fleet - * @param {number} fleetId - Fleet ID - * @returns {Promise} Total ship count - */ - async getFleetShipCount(fleetId) { - try { - const result = await db('fleet_ships') - .sum('quantity as total') - .where('fleet_id', fleetId) - .first(); - - return result.total || 0; - } catch (error) { - logger.error('Failed to get fleet ship count', { - fleetId, - error: error.message - }); - return 0; - } - } -} - -module.exports = FleetService; \ No newline at end of file diff --git a/src/services/fleet/ShipDesignService.js b/src/services/fleet/ShipDesignService.js deleted file mode 100644 index 383d77e..0000000 --- a/src/services/fleet/ShipDesignService.js +++ /dev/null @@ -1,466 +0,0 @@ -/** - * Ship Design Service - * Handles ship design availability, prerequisites, and construction calculations - */ - -const logger = require('../../utils/logger'); -const db = require('../../database/connection'); -const { - SHIP_DESIGNS, - SHIP_CLASSES, - HULL_TYPES, - getShipDesignById, - getShipDesignsByClass, - getAvailableShipDesigns, - validateShipDesignAvailability, - calculateShipCost, - calculateBuildTime -} = require('../../data/ship-designs'); - -class ShipDesignService { - constructor(gameEventService = null) { - this.gameEventService = gameEventService; - } - - /** - * Get all available ship designs for a player - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Available ship designs - */ - async getAvailableDesigns(playerId, correlationId) { - try { - logger.info('Getting available ship designs for player', { - correlationId, - playerId - }); - - // Get completed technologies for this player - const completedTechs = await db('player_research') - .select('technology_id') - .where('player_id', playerId) - .where('status', 'completed'); - - const completedTechIds = completedTechs.map(tech => tech.technology_id); - - // Get available ship designs based on technology prerequisites - const availableDesigns = getAvailableShipDesigns(completedTechIds); - - // Get any custom designs for this player - const customDesigns = await db('ship_designs') - .select('*') - .where(function() { - this.where('player_id', playerId) - .orWhere('is_public', true); - }) - .where('is_active', true); - - // Combine standard and custom designs - const allDesigns = [ - ...availableDesigns.map(design => ({ - ...design, - design_type: 'standard', - is_available: true - })), - ...customDesigns.map(design => ({ - ...design, - design_type: 'custom', - is_available: true, - // Parse JSON fields if they're strings - components: typeof design.components === 'string' - ? JSON.parse(design.components) - : design.components, - stats: typeof design.stats === 'string' - ? JSON.parse(design.stats) - : design.stats, - cost: typeof design.cost === 'string' - ? JSON.parse(design.cost) - : design.cost - })) - ]; - - logger.debug('Available ship designs retrieved', { - correlationId, - playerId, - standardDesigns: availableDesigns.length, - customDesigns: customDesigns.length, - totalDesigns: allDesigns.length - }); - - return allDesigns; - - } catch (error) { - logger.error('Failed to get available ship designs', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Get ship designs by class for a player - * @param {number} playerId - Player ID - * @param {string} shipClass - Ship class filter - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Ship designs in the specified class - */ - async getDesignsByClass(playerId, shipClass, correlationId) { - try { - logger.info('Getting ship designs by class for player', { - correlationId, - playerId, - shipClass - }); - - const allDesigns = await this.getAvailableDesigns(playerId, correlationId); - const filteredDesigns = allDesigns.filter(design => - design.ship_class === shipClass - ); - - logger.debug('Ship designs by class retrieved', { - correlationId, - playerId, - shipClass, - count: filteredDesigns.length - }); - - return filteredDesigns; - - } catch (error) { - logger.error('Failed to get ship designs by class', { - correlationId, - playerId, - shipClass, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Validate if a player can build a specific ship design - * @param {number} playerId - Player ID - * @param {number} designId - Ship design ID - * @param {number} quantity - Number of ships to build - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Validation result - */ - async validateShipConstruction(playerId, designId, quantity, correlationId) { - try { - logger.info('Validating ship construction for player', { - correlationId, - playerId, - designId, - quantity - }); - - // Get ship design (standard or custom) - let design = getShipDesignById(designId); - let isCustomDesign = false; - - if (!design) { - // Check for custom design - const customDesign = await db('ship_designs') - .select('*') - .where('id', designId) - .where(function() { - this.where('player_id', playerId) - .orWhere('is_public', true); - }) - .where('is_active', true) - .first(); - - if (customDesign) { - design = { - ...customDesign, - components: typeof customDesign.components === 'string' - ? JSON.parse(customDesign.components) - : customDesign.components, - stats: typeof customDesign.stats === 'string' - ? JSON.parse(customDesign.stats) - : customDesign.stats, - base_cost: typeof customDesign.cost === 'string' - ? JSON.parse(customDesign.cost) - : customDesign.cost, - tech_requirements: [] // Custom designs assume tech requirements are met - }; - isCustomDesign = true; - } - } - - if (!design) { - return { - valid: false, - error: 'Ship design not found or not available' - }; - } - - // For standard designs, check technology requirements - if (!isCustomDesign) { - const completedTechs = await db('player_research') - .select('technology_id') - .where('player_id', playerId) - .where('status', 'completed'); - - const completedTechIds = completedTechs.map(tech => tech.technology_id); - const techValidation = validateShipDesignAvailability(designId, completedTechIds); - - if (!techValidation.valid) { - return techValidation; - } - } - - // Get construction bonuses from completed research - const bonuses = await this.getConstructionBonuses(playerId, correlationId); - - // Calculate actual costs and build time - const actualCost = calculateShipCost(design, bonuses); - const actualBuildTime = calculateBuildTime(design, bonuses); - - // Calculate total costs for the quantity - const totalCost = {}; - Object.entries(actualCost).forEach(([resource, cost]) => { - totalCost[resource] = cost * quantity; - }); - - // Check player resources - const playerResources = await db('player_resources') - .select([ - 'resource_types.name', - 'player_resources.amount' - ]) - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId); - - const resourceMap = new Map(); - playerResources.forEach(resource => { - resourceMap.set(resource.name, resource.amount); - }); - - // Check for insufficient resources - const insufficientResources = []; - Object.entries(totalCost).forEach(([resourceName, cost]) => { - const available = resourceMap.get(resourceName) || 0; - if (available < cost) { - insufficientResources.push({ - resource: resourceName, - required: cost, - available: available, - missing: cost - available - }); - } - }); - - if (insufficientResources.length > 0) { - return { - valid: false, - error: 'Insufficient resources for construction', - insufficientResources - }; - } - - const result = { - valid: true, - design: design, - quantity: quantity, - total_cost: totalCost, - build_time_per_ship: actualBuildTime, - total_build_time: actualBuildTime * quantity, - bonuses_applied: bonuses, - is_custom_design: isCustomDesign - }; - - logger.debug('Ship construction validation completed', { - correlationId, - playerId, - designId, - quantity, - valid: result.valid, - totalBuildTime: result.total_build_time - }); - - return result; - - } catch (error) { - logger.error('Failed to validate ship construction', { - correlationId, - playerId, - designId, - quantity, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Get construction bonuses from completed technologies - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Construction bonuses - */ - async getConstructionBonuses(playerId, correlationId) { - try { - // Get completed technologies - const completedTechs = await db('player_research') - .select('technology_id') - .where('player_id', playerId) - .where('status', 'completed'); - - const completedTechIds = completedTechs.map(tech => tech.technology_id); - - // Calculate bonuses (this could be expanded based on technology effects) - const bonuses = { - construction_cost_reduction: 0, - construction_speed_bonus: 0, - material_efficiency: 0 - }; - - // Basic bonuses from key technologies - if (completedTechIds.includes(6)) { // Industrial Automation - bonuses.construction_speed_bonus += 0.15; - bonuses.construction_cost_reduction += 0.05; - } - - if (completedTechIds.includes(11)) { // Advanced Manufacturing - bonuses.construction_speed_bonus += 0.25; - bonuses.material_efficiency += 0.3; - } - - if (completedTechIds.includes(16)) { // Nanotechnology - bonuses.construction_speed_bonus += 0.4; - bonuses.construction_cost_reduction += 0.2; - bonuses.material_efficiency += 0.6; - } - - logger.debug('Construction bonuses calculated', { - correlationId, - playerId, - bonuses, - completedTechCount: completedTechIds.length - }); - - return bonuses; - - } catch (error) { - logger.error('Failed to get construction bonuses', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Get ship design details - * @param {number} designId - Ship design ID - * @param {number} playerId - Player ID (for custom designs) - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Ship design details - */ - async getDesignDetails(designId, playerId, correlationId) { - try { - logger.info('Getting ship design details', { - correlationId, - playerId, - designId - }); - - // Try standard design first - let design = getShipDesignById(designId); - let isCustomDesign = false; - - if (!design) { - // Check for custom design - const customDesign = await db('ship_designs') - .select('*') - .where('id', designId) - .where(function() { - this.where('player_id', playerId) - .orWhere('is_public', true); - }) - .where('is_active', true) - .first(); - - if (customDesign) { - design = { - ...customDesign, - components: typeof customDesign.components === 'string' - ? JSON.parse(customDesign.components) - : customDesign.components, - stats: typeof customDesign.stats === 'string' - ? JSON.parse(customDesign.stats) - : customDesign.stats, - base_cost: typeof customDesign.cost === 'string' - ? JSON.parse(customDesign.cost) - : customDesign.cost - }; - isCustomDesign = true; - } - } - - if (!design) { - const error = new Error('Ship design not found'); - error.statusCode = 404; - throw error; - } - - // Get construction bonuses - const bonuses = await this.getConstructionBonuses(playerId, correlationId); - - // Calculate modified costs and build time - const modifiedCost = calculateShipCost(design, bonuses); - const modifiedBuildTime = calculateBuildTime(design, bonuses); - - const result = { - ...design, - is_custom_design: isCustomDesign, - modified_cost: modifiedCost, - modified_build_time: modifiedBuildTime, - bonuses_applied: bonuses, - hull_type_stats: HULL_TYPES[design.hull_type] || null - }; - - logger.debug('Ship design details retrieved', { - correlationId, - playerId, - designId, - isCustomDesign, - shipClass: design.ship_class - }); - - return result; - - } catch (error) { - logger.error('Failed to get ship design details', { - correlationId, - playerId, - designId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Get all ship classes and their characteristics - * @returns {Object} Ship classes and hull types data - */ - getShipClassesInfo() { - return { - ship_classes: SHIP_CLASSES, - hull_types: HULL_TYPES, - total_designs: SHIP_DESIGNS.length - }; - } -} - -module.exports = ShipDesignService; \ No newline at end of file diff --git a/src/services/galaxy/ColonyService.js b/src/services/galaxy/ColonyService.js deleted file mode 100644 index 054d3a5..0000000 --- a/src/services/galaxy/ColonyService.js +++ /dev/null @@ -1,702 +0,0 @@ -/** - * Colony Service - * Handles all colony-related business logic including creation, management, and building operations - */ - -const db = require('../../database/connection'); -const logger = require('../../utils/logger'); -const { ValidationError, ConflictError, NotFoundError, ServiceError } = require('../../middleware/error.middleware'); - -class ColonyService { - constructor(gameEventService = null) { - this.gameEventService = gameEventService; - } - /** - * Create a new colony - * @param {number} playerId - Player ID - * @param {Object} colonyData - Colony creation data - * @param {string} colonyData.name - Colony name - * @param {string} colonyData.coordinates - Galaxy coordinates - * @param {number} colonyData.planet_type_id - Planet type ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Created colony data - */ - async createColony(playerId, colonyData, correlationId) { - try { - const { name, coordinates, planet_type_id } = colonyData; - - logger.info('Colony creation initiated', { - correlationId, - playerId, - name, - coordinates, - planet_type_id, - }); - - // Validate input data - await this.validateColonyData({ name, coordinates, planet_type_id }); - - // Check if coordinates are already taken - const existingColony = await this.getColonyByCoordinates(coordinates); - if (existingColony) { - throw new ConflictError('Coordinates already occupied'); - } - - // Check player colony limit - const playerColonyCount = await this.getPlayerColonyCount(playerId); - const maxColonies = parseInt(process.env.MAX_COLONIES_PER_PLAYER) || 10; - if (playerColonyCount >= maxColonies) { - throw new ConflictError(`Maximum number of colonies reached (${maxColonies})`); - } - - // Validate planet type exists - const planetType = await this.getPlanetTypeById(planet_type_id); - if (!planetType) { - throw new ValidationError('Invalid planet type'); - } - - // Get sector from coordinates - const sectorCoordinates = this.extractSectorFromCoordinates(coordinates); - const sector = await this.getSectorByCoordinates(sectorCoordinates); - - // Database transaction for atomic operation - const colony = await db.transaction(async (trx) => { - // Create colony - const [newColony] = await trx('colonies') - .insert({ - player_id: playerId, - name: name.trim(), - coordinates: coordinates.toUpperCase(), - sector_id: sector?.id || null, - planet_type_id, - population: 100, // Starting population - max_population: planetType.max_population, - morale: 100, - loyalty: 100, - founded_at: new Date(), - last_updated: new Date(), - }) - .returning('*'); - - // Create initial buildings (Command Center is required) - await this.createInitialBuildings(newColony.id, trx); - - // Initialize colony resource production tracking - await this.initializeColonyResources(newColony.id, planetType, trx); - - // Update player colony count - await trx('player_stats') - .where('player_id', playerId) - .increment('colonies_count', 1); - - logger.info('Colony created successfully', { - correlationId, - colonyId: newColony.id, - playerId, - name: newColony.name, - coordinates: newColony.coordinates, - }); - - return newColony; - }); - - // Return colony with additional data - const colonyDetails = await this.getColonyDetails(colony.id, correlationId); - - // Emit WebSocket event for colony creation - if (this.gameEventService) { - this.gameEventService.emitColonyCreated(playerId, colonyDetails, correlationId); - } - - return colonyDetails; - - } catch (error) { - logger.error('Colony creation failed', { - correlationId, - playerId, - colonyData, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError || error instanceof ConflictError) { - throw error; - } - throw new ServiceError('Failed to create colony', error); - } - } - - /** - * Get colonies owned by a player - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} List of player colonies - */ - async getPlayerColonies(playerId, correlationId) { - try { - logger.info('Fetching player colonies', { - correlationId, - playerId, - }); - - const colonies = await db('colonies') - .select([ - 'colonies.*', - 'planet_types.name as planet_type_name', - 'planet_types.description as planet_type_description', - 'galaxy_sectors.name as sector_name', - 'galaxy_sectors.danger_level', - ]) - .leftJoin('planet_types', 'colonies.planet_type_id', 'planet_types.id') - .leftJoin('galaxy_sectors', 'colonies.sector_id', 'galaxy_sectors.id') - .where('colonies.player_id', playerId) - .orderBy('colonies.founded_at', 'asc'); - - // Get building counts for each colony - const coloniesWithBuildings = await Promise.all(colonies.map(async (colony) => { - const buildingCount = await db('colony_buildings') - .where('colony_id', colony.id) - .count('* as count') - .first(); - - return { - ...colony, - buildingCount: parseInt(buildingCount.count) || 0, - }; - })); - - logger.info('Player colonies retrieved', { - correlationId, - playerId, - colonyCount: colonies.length, - }); - - return coloniesWithBuildings; - - } catch (error) { - logger.error('Failed to fetch player colonies', { - correlationId, - playerId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve player colonies', error); - } - } - - /** - * Get detailed colony information - * @param {number} colonyId - Colony ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Detailed colony data - */ - async getColonyDetails(colonyId, correlationId) { - try { - logger.info('Fetching colony details', { - correlationId, - colonyId, - }); - - // Get colony basic information - const colony = await db('colonies') - .select([ - 'colonies.*', - 'planet_types.name as planet_type_name', - 'planet_types.description as planet_type_description', - 'planet_types.base_resources', - 'planet_types.resource_modifiers', - 'galaxy_sectors.name as sector_name', - 'galaxy_sectors.danger_level', - 'galaxy_sectors.description as sector_description', - ]) - .leftJoin('planet_types', 'colonies.planet_type_id', 'planet_types.id') - .leftJoin('galaxy_sectors', 'colonies.sector_id', 'galaxy_sectors.id') - .where('colonies.id', colonyId) - .first(); - - if (!colony) { - throw new NotFoundError('Colony not found'); - } - - // Get colony buildings - const buildings = await db('colony_buildings') - .select([ - 'colony_buildings.*', - 'building_types.name as building_name', - 'building_types.description as building_description', - 'building_types.category', - 'building_types.max_level', - 'building_types.base_cost', - 'building_types.base_production', - 'building_types.special_effects', - ]) - .join('building_types', 'colony_buildings.building_type_id', 'building_types.id') - .where('colony_buildings.colony_id', colonyId) - .orderBy('building_types.category') - .orderBy('building_types.name'); - - // Get colony resource production - const resources = await db('colony_resource_production') - .select([ - 'colony_resource_production.*', - 'resource_types.name as resource_name', - 'resource_types.description as resource_description', - 'resource_types.category as resource_category', - ]) - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colony_resource_production.colony_id', colonyId); - - const colonyDetails = { - ...colony, - buildings: buildings || [], - resources: resources || [], - }; - - logger.info('Colony details retrieved', { - correlationId, - colonyId, - buildingCount: buildings.length, - resourceCount: resources.length, - }); - - return colonyDetails; - - } catch (error) { - logger.error('Failed to fetch colony details', { - correlationId, - colonyId, - error: error.message, - stack: error.stack, - }); - - if (error instanceof NotFoundError) { - throw error; - } - throw new ServiceError('Failed to retrieve colony details', error); - } - } - - /** - * Construct a building in a colony - * @param {number} colonyId - Colony ID - * @param {number} buildingTypeId - Building type ID - * @param {number} playerId - Player ID (for authorization) - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Construction result - */ - async constructBuilding(colonyId, buildingTypeId, playerId, correlationId) { - try { - logger.info('Building construction initiated', { - correlationId, - colonyId, - buildingTypeId, - playerId, - }); - - // Verify colony ownership - const colony = await this.verifyColonyOwnership(colonyId, playerId); - if (!colony) { - throw new NotFoundError('Colony not found or access denied'); - } - - // Check if building already exists - const existingBuilding = await db('colony_buildings') - .where('colony_id', colonyId) - .where('building_type_id', buildingTypeId) - .first(); - - if (existingBuilding) { - throw new ConflictError('Building already exists in this colony'); - } - - // Get building type information - const buildingType = await db('building_types') - .where('id', buildingTypeId) - .where('is_active', true) - .first(); - - if (!buildingType) { - throw new ValidationError('Invalid building type'); - } - - // Check if building is unique and already exists - if (buildingType.is_unique) { - const existingUniqueBuilding = await db('colony_buildings') - .where('colony_id', colonyId) - .where('building_type_id', buildingTypeId) - .first(); - - if (existingUniqueBuilding) { - throw new ConflictError('This building type can only be built once per colony'); - } - } - - // Check prerequisites (TODO: Implement prerequisite checking) - // await this.checkBuildingPrerequisites(colonyId, buildingType.prerequisites); - - // Check resource costs - const canAfford = await this.checkBuildingCosts(playerId, buildingType.base_cost); - if (!canAfford.canAfford) { - throw new ValidationError('Insufficient resources', { missing: canAfford.missing }); - } - - // Database transaction for atomic operation - const result = await db.transaction(async (trx) => { - // Deduct resources from player - await this.deductResources(playerId, buildingType.base_cost, trx); - - // Create building (instant construction for MVP) - const [newBuilding] = await trx('colony_buildings') - .insert({ - colony_id: colonyId, - building_type_id: buildingTypeId, - level: 1, - health_percentage: 100, - is_under_construction: false, - created_at: new Date(), - updated_at: new Date(), - }) - .returning('*'); - - // Update colony resource production if this building produces resources - if (buildingType.base_production && Object.keys(buildingType.base_production).length > 0) { - await this.updateColonyResourceProduction(colonyId, buildingType, trx); - } - - logger.info('Building constructed successfully', { - correlationId, - colonyId, - buildingId: newBuilding.id, - buildingTypeId, - playerId, - }); - - // Emit WebSocket event for building construction - if (this.gameEventService) { - this.gameEventService.emitBuildingConstructed(playerId, colonyId, newBuilding, correlationId); - } - - return newBuilding; - }); - - return result; - - } catch (error) { - logger.error('Building construction failed', { - correlationId, - colonyId, - buildingTypeId, - playerId, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError || error instanceof ConflictError || error instanceof NotFoundError) { - throw error; - } - throw new ServiceError('Failed to construct building', error); - } - } - - /** - * Get available building types - * @param {string} correlationId - Request correlation ID - * @returns {Promise} List of available building types - */ - async getAvailableBuildingTypes(correlationId) { - try { - logger.info('Fetching available building types', { correlationId }); - - const buildingTypes = await db('building_types') - .select('*') - .where('is_active', true) - .orderBy('category') - .orderBy('name'); - - logger.info('Building types retrieved', { - correlationId, - count: buildingTypes.length, - }); - - return buildingTypes; - - } catch (error) { - logger.error('Failed to fetch building types', { - correlationId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve building types', error); - } - } - - // Helper methods - - /** - * Validate colony creation data - * @param {Object} data - Colony data to validate - * @returns {Promise} - */ - async validateColonyData(data) { - const { name, coordinates, planet_type_id } = data; - - if (!name || typeof name !== 'string' || name.trim().length < 3 || name.trim().length > 50) { - throw new ValidationError('Colony name must be between 3 and 50 characters'); - } - - if (!this.isValidCoordinates(coordinates)) { - throw new ValidationError('Invalid coordinates format. Expected format: A3-91-X'); - } - - if (!planet_type_id || typeof planet_type_id !== 'number' || planet_type_id < 1) { - throw new ValidationError('Valid planet type ID is required'); - } - } - - /** - * Validate galaxy coordinates format - * @param {string} coordinates - Coordinates to validate - * @returns {boolean} True if valid - */ - isValidCoordinates(coordinates) { - // Format: A3-91-X (Letter+Number-Number-Letter) - const coordinatePattern = /^[A-Z]\d+-\d+-[A-Z]$/; - return coordinatePattern.test(coordinates); - } - - /** - * Extract sector coordinates from full coordinates - * @param {string} coordinates - Full coordinates (e.g., "A3-91-X") - * @returns {string} Sector coordinates (e.g., "A3") - */ - extractSectorFromCoordinates(coordinates) { - const match = coordinates.match(/^([A-Z]\d+)-/); - return match ? match[1] : null; - } - - /** - * Get colony by coordinates - * @param {string} coordinates - Galaxy coordinates - * @returns {Promise} Colony data or null - */ - async getColonyByCoordinates(coordinates) { - try { - return await db('colonies') - .where('coordinates', coordinates.toUpperCase()) - .first(); - } catch (error) { - logger.error('Failed to find colony by coordinates', { error: error.message }); - return null; - } - } - - /** - * Get player colony count - * @param {number} playerId - Player ID - * @returns {Promise} Number of colonies owned by player - */ - async getPlayerColonyCount(playerId) { - try { - const result = await db('colonies') - .where('player_id', playerId) - .count('* as count') - .first(); - return parseInt(result.count) || 0; - } catch (error) { - logger.error('Failed to get player colony count', { error: error.message }); - return 0; - } - } - - /** - * Get planet type by ID - * @param {number} planetTypeId - Planet type ID - * @returns {Promise} Planet type data or null - */ - async getPlanetTypeById(planetTypeId) { - try { - return await db('planet_types') - .where('id', planetTypeId) - .where('is_active', true) - .first(); - } catch (error) { - logger.error('Failed to find planet type', { error: error.message }); - return null; - } - } - - /** - * Get sector by coordinates - * @param {string} coordinates - Sector coordinates - * @returns {Promise} Sector data or null - */ - async getSectorByCoordinates(coordinates) { - try { - return await db('galaxy_sectors') - .where('coordinates', coordinates) - .first(); - } catch (error) { - logger.error('Failed to find sector', { error: error.message }); - return null; - } - } - - /** - * Create initial buildings for a new colony - * @param {number} colonyId - Colony ID - * @param {Object} trx - Database transaction - * @returns {Promise} - */ - async createInitialBuildings(colonyId, trx) { - // Get Command Center building type - const commandCenter = await trx('building_types') - .where('name', 'Command Center') - .first(); - - if (commandCenter) { - await trx('colony_buildings').insert({ - colony_id: colonyId, - building_type_id: commandCenter.id, - level: 1, - health_percentage: 100, - is_under_construction: false, - created_at: new Date(), - updated_at: new Date(), - }); - } - } - - /** - * Initialize colony resource production tracking - * @param {number} colonyId - Colony ID - * @param {Object} planetType - Planet type data - * @param {Object} trx - Database transaction - * @returns {Promise} - */ - async initializeColonyResources(colonyId, planetType, trx) { - // Get all resource types - const resourceTypes = await trx('resource_types') - .where('is_active', true); - - const baseResources = planetType.base_resources || {}; - const modifiers = planetType.resource_modifiers || {}; - - // Initialize production tracking for each resource type - for (const resourceType of resourceTypes) { - const resourceName = resourceType.name; - const initialStored = baseResources[resourceName] || 0; - const modifier = modifiers[resourceName] || 1.0; - - await trx('colony_resource_production').insert({ - colony_id: colonyId, - resource_type_id: resourceType.id, - production_rate: 0, // Will be calculated based on buildings - consumption_rate: 0, - current_stored: initialStored, - storage_capacity: 10000, // Default storage capacity - last_calculated: new Date(), - }); - } - } - - /** - * Verify colony ownership - * @param {number} colonyId - Colony ID - * @param {number} playerId - Player ID - * @returns {Promise} Colony data if owned by player - */ - async verifyColonyOwnership(colonyId, playerId) { - try { - return await db('colonies') - .where('id', colonyId) - .where('player_id', playerId) - .first(); - } catch (error) { - logger.error('Failed to verify colony ownership', { error: error.message }); - return null; - } - } - - /** - * Check if player can afford building costs - * @param {number} playerId - Player ID - * @param {Object} costs - Resource costs - * @returns {Promise} Affordability result - */ - async checkBuildingCosts(playerId, costs) { - try { - const result = { canAfford: true, missing: {} }; - - // Get player resources - const playerResources = await db('player_resources') - .select([ - 'player_resources.amount', - 'resource_types.name as resource_name', - ]) - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId); - - const resourceMap = {}; - playerResources.forEach(resource => { - resourceMap[resource.resource_name] = parseInt(resource.amount); - }); - - // Check each cost requirement - for (const [resourceName, requiredAmount] of Object.entries(costs)) { - const available = resourceMap[resourceName] || 0; - if (available < requiredAmount) { - result.canAfford = false; - result.missing[resourceName] = requiredAmount - available; - } - } - - return result; - - } catch (error) { - logger.error('Failed to check building costs', { error: error.message }); - return { canAfford: false, missing: {} }; - } - } - - /** - * Deduct resources from player - * @param {number} playerId - Player ID - * @param {Object} costs - Resource costs to deduct - * @param {Object} trx - Database transaction - * @returns {Promise} - */ - async deductResources(playerId, costs, trx) { - for (const [resourceName, amount] of Object.entries(costs)) { - await trx('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId) - .where('resource_types.name', resourceName) - .decrement('player_resources.amount', amount) - .update('player_resources.last_updated', new Date()); - } - } - - /** - * Update colony resource production after building construction - * @param {number} colonyId - Colony ID - * @param {Object} buildingType - Building type data - * @param {Object} trx - Database transaction - * @returns {Promise} - */ - async updateColonyResourceProduction(colonyId, buildingType, trx) { - if (!buildingType.base_production) return; - - for (const [resourceName, productionAmount] of Object.entries(buildingType.base_production)) { - await trx('colony_resource_production') - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colony_resource_production.colony_id', colonyId) - .where('resource_types.name', resourceName) - .increment('colony_resource_production.production_rate', productionAmount) - .update('colony_resource_production.last_calculated', new Date()); - } - } -} - -module.exports = ColonyService; diff --git a/src/services/game-tick.service.js b/src/services/game-tick.service.js index 65a44a9..107bd46 100644 --- a/src/services/game-tick.service.js +++ b/src/services/game-tick.service.js @@ -23,13 +23,13 @@ class GameTickService { try { // Load configuration await this.loadConfig(); - + // Get current tick number await this.loadCurrentTick(); - + // Start the cron job this.startTickScheduler(); - + this.isInitialized = true; logger.info('Game tick service initialized', { tickInterval: this.config.tick_interval_ms, @@ -96,7 +96,7 @@ class GameTickService { */ createCronPattern(intervalMs) { const intervalMinutes = Math.floor(intervalMs / 60000); - + if (intervalMinutes === 1) { return '* * * * *'; // Every minute } else if (intervalMinutes === 5) { @@ -122,148 +122,28 @@ class GameTickService { logger.info('Starting game tick', { tickNumber }); - // Initialize processing state - this.isProcessing = true; - this.processingStartTime = startTime; - this.failedUserGroups = new Set(); - - let totalResourcesProduced = 0; - let totalPlayersProcessed = 0; - let totalSystemErrors = 0; - const globalSystemMetrics = { - resources: { totalProcessed: 0, totalErrors: 0, avgDuration: 0 }, - buildings: { totalProcessed: 0, totalErrors: 0, avgDuration: 0 }, - research: { totalProcessed: 0, totalErrors: 0, avgDuration: 0 }, - fleetMovements: { totalProcessed: 0, totalErrors: 0, avgDuration: 0 }, - fleetConstruction: { totalProcessed: 0, totalErrors: 0, avgDuration: 0 } - }; - // Process each user group for (let userGroup = 1; userGroup <= this.config.max_user_groups; userGroup++) { - const groupResult = await this.processUserGroupTick(tickNumber, userGroup); - if (groupResult.totalResourcesProduced) { - totalResourcesProduced += groupResult.totalResourcesProduced; - } - if (groupResult.processedPlayers) { - totalPlayersProcessed += groupResult.processedPlayers; - } - if (groupResult.systemMetrics) { - // Aggregate system metrics - this.aggregateSystemMetrics(globalSystemMetrics, groupResult.systemMetrics); - } - if (groupResult.systemErrors) { - totalSystemErrors += groupResult.systemErrors; - } + await this.processUserGroupTick(tickNumber, userGroup); } const endTime = new Date(); const duration = endTime.getTime() - startTime.getTime(); - this.isProcessing = false; - this.processingStartTime = null; - - // Store last tick metrics - this.lastTickMetrics = { - tickNumber, - duration, - completedAt: endTime, - userGroupsProcessed: this.config.max_user_groups || 10, - failedGroups: this.failedUserGroups.size, - totalResourcesProduced, - totalPlayersProcessed, - }; - - // Enhanced logging with system-specific metrics logger.info('Game tick completed', { tickNumber, duration: `${duration}ms`, - userGroupsProcessed: this.config.max_user_groups || 10, - failedGroups: this.failedUserGroups.size, - totalResourcesProduced, - totalPlayersProcessed, - systemMetrics: { - resources: { - avgDuration: `${globalSystemMetrics.resources.avgDuration.toFixed(2)}ms`, - totalProcessed: globalSystemMetrics.resources.totalProcessed, - totalErrors: globalSystemMetrics.resources.totalErrors, - successRate: globalSystemMetrics.resources.totalProcessed > 0 - ? `${(((globalSystemMetrics.resources.totalProcessed - globalSystemMetrics.resources.totalErrors) / globalSystemMetrics.resources.totalProcessed) * 100).toFixed(1)}%` - : '0%' - }, - research: { - avgDuration: `${globalSystemMetrics.research.avgDuration.toFixed(2)}ms`, - totalProcessed: globalSystemMetrics.research.totalProcessed, - totalErrors: globalSystemMetrics.research.totalErrors, - successRate: globalSystemMetrics.research.totalProcessed > 0 - ? `${(((globalSystemMetrics.research.totalProcessed - globalSystemMetrics.research.totalErrors) / globalSystemMetrics.research.totalProcessed) * 100).toFixed(1)}%` - : '0%' - }, - fleets: { - movements: { - avgDuration: `${globalSystemMetrics.fleetMovements.avgDuration.toFixed(2)}ms`, - totalProcessed: globalSystemMetrics.fleetMovements.totalProcessed, - totalErrors: globalSystemMetrics.fleetMovements.totalErrors - }, - construction: { - avgDuration: `${globalSystemMetrics.fleetConstruction.avgDuration.toFixed(2)}ms`, - totalProcessed: globalSystemMetrics.fleetConstruction.totalProcessed, - totalErrors: globalSystemMetrics.fleetConstruction.totalErrors - } - }, - buildings: { - avgDuration: `${globalSystemMetrics.buildings.avgDuration.toFixed(2)}ms`, - totalProcessed: globalSystemMetrics.buildings.totalProcessed, - totalErrors: globalSystemMetrics.buildings.totalErrors - } - }, - performance: { - playersPerSecond: totalPlayersProcessed > 0 ? Math.round((totalPlayersProcessed * 1000) / duration) : 0, - resourcesPerSecond: totalResourcesProduced > 0 ? Math.round((totalResourcesProduced * 1000) / duration) : 0, - avgPlayerProcessingTime: totalPlayersProcessed > 0 ? `${(duration / totalPlayersProcessed).toFixed(2)}ms` : '0ms' - } }); - - // Get service locator for game event service - const serviceLocator = require('./ServiceLocator'); - const gameEventService = serviceLocator.get('gameEventService'); - - // Emit game tick completion events - if (gameEventService) { - // Emit detailed tick completion event - gameEventService.emitGameTickCompleted( - tickNumber, - this.lastTickMetrics, - `tick-${tickNumber}-completed`, - ); - - // Also emit system announcement for major ticks - if (tickNumber % 10 === 0 || totalResourcesProduced > 10000) { - gameEventService.emitSystemAnnouncement( - `Game tick ${tickNumber} completed - ${totalResourcesProduced} resources produced`, - 'info', - { - tickNumber, - duration, - totalResourcesProduced, - totalPlayersProcessed, - timestamp: endTime.toISOString(), - }, - `tick-${tickNumber}-announcement`, - ); - } - } } /** * Process tick for a specific user group * @param {number} tickNumber - Current tick number * @param {number} userGroup - User group to process - * @returns {Promise} Processing results */ async processUserGroupTick(tickNumber, userGroup) { const startTime = new Date(); let processedPlayers = 0; - let totalResourcesProduced = 0; let attempt = 0; while (attempt < this.config.max_retry_attempts) { @@ -277,38 +157,9 @@ class GameTickService { .where('account_status', 'active') .select('id'); - // Initialize group-level system metrics - const groupSystemMetrics = { - resources: { processed: 0, duration: 0, errors: 0 }, - buildings: { processed: 0, duration: 0, errors: 0 }, - research: { processed: 0, duration: 0, errors: 0 }, - fleetMovements: { processed: 0, duration: 0, errors: 0 }, - fleetConstruction: { processed: 0, duration: 0, errors: 0 } - }; - // Process each player for (const player of players) { - const playerResult = await this.processPlayerTick(tickNumber, player.id); - if (playerResult && playerResult.totalResourcesProduced) { - totalResourcesProduced += playerResult.totalResourcesProduced; - } - - // Aggregate player system metrics to group level - if (playerResult && playerResult.systemMetrics) { - Object.keys(groupSystemMetrics).forEach(systemName => { - const playerMetric = playerResult.systemMetrics[systemName]; - const groupMetric = groupSystemMetrics[systemName]; - - if (playerMetric) { - if (playerMetric.processed) groupMetric.processed++; - if (playerMetric.error) groupMetric.errors++; - if (playerMetric.duration > 0) { - groupMetric.duration = (groupMetric.duration + playerMetric.duration) / Math.max(1, groupMetric.processed); - } - } - }); - } - + await this.processPlayerTick(tickNumber, player.id); processedPlayers++; } @@ -319,22 +170,13 @@ class GameTickService { tickNumber, userGroup, processedPlayers, - totalResourcesProduced, attempt: attempt + 1, }); - return { - processedPlayers, - totalResourcesProduced, - userGroup, - success: true, - systemMetrics: groupSystemMetrics - }; + break; // Success, exit retry loop } catch (error) { attempt++; - this.failedUserGroups.add(userGroup); - logger.error('User group tick failed', { tickNumber, userGroup, @@ -345,19 +187,11 @@ class GameTickService { if (attempt >= this.config.max_retry_attempts) { // Max retries reached, log failure await this.logTickFailure(tickNumber, userGroup, attempt, error.message); - + // Apply bonus tick if configured if (attempt >= this.config.bonus_tick_threshold) { await this.applyBonusTick(tickNumber, userGroup); } - - return { - processedPlayers: 0, - totalResourcesProduced: 0, - userGroup, - success: false, - error: error.message, - }; } else { // Wait before retry await this.sleep(this.config.retry_delay_ms); @@ -370,18 +204,8 @@ class GameTickService { * Process tick for a single player * @param {number} tickNumber - Current tick number * @param {number} playerId - Player ID - * @returns {Promise} Processing results */ async processPlayerTick(tickNumber, playerId) { - const startTime = process.hrtime.bigint(); - const systemMetrics = { - resources: { processed: false, duration: 0, error: null }, - buildings: { processed: false, duration: 0, error: null }, - research: { processed: false, duration: 0, error: null }, - fleetMovements: { processed: false, duration: 0, error: null }, - fleetConstruction: { processed: false, duration: 0, error: null } - }; - try { // Use lock to prevent concurrent processing const lockKey = `player_tick:${playerId}`; @@ -389,132 +213,36 @@ class GameTickService { if (!lockToken) { logger.warn('Could not acquire player tick lock', { playerId, tickNumber }); - return { totalResourcesProduced: 0, systemMetrics }; + return; } - let totalResourcesProduced = 0; - try { - // Process resource production with timing - const resourceStart = process.hrtime.bigint(); - try { - const resourceResult = await this.processResourceProduction(playerId, tickNumber); - if (resourceResult && resourceResult.totalResourcesProduced) { - totalResourcesProduced += resourceResult.totalResourcesProduced; - } - systemMetrics.resources.processed = true; - systemMetrics.resources.duration = Number(process.hrtime.bigint() - resourceStart) / 1000000; - } catch (error) { - systemMetrics.resources.error = error.message; - throw error; - } - - // Process building construction with timing and retry logic - const buildingStart = process.hrtime.bigint(); - try { - await this.processBuildingConstruction(playerId, tickNumber); - systemMetrics.buildings.processed = true; - systemMetrics.buildings.duration = Number(process.hrtime.bigint() - buildingStart) / 1000000; - } catch (error) { - systemMetrics.buildings.error = error.message; - logger.error('Building construction processing failed', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - // Continue processing other systems even if this fails - } - - // Process research with timing and retry logic - const researchStart = process.hrtime.bigint(); - try { - await this.processResearch(playerId, tickNumber); - systemMetrics.research.processed = true; - systemMetrics.research.duration = Number(process.hrtime.bigint() - researchStart) / 1000000; - } catch (error) { - systemMetrics.research.error = error.message; - logger.error('Research processing failed', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - // Continue processing other systems even if this fails - } - - // Process fleet movements with timing and retry logic - const fleetMovementStart = process.hrtime.bigint(); - try { - await this.processFleetMovements(playerId, tickNumber); - systemMetrics.fleetMovements.processed = true; - systemMetrics.fleetMovements.duration = Number(process.hrtime.bigint() - fleetMovementStart) / 1000000; - } catch (error) { - systemMetrics.fleetMovements.error = error.message; - logger.error('Fleet movement processing failed', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - // Continue processing other systems even if this fails - } - - // Process fleet construction with timing and retry logic - const fleetConstructionStart = process.hrtime.bigint(); - try { - await this.processFleetConstruction(playerId, tickNumber); - systemMetrics.fleetConstruction.processed = true; - systemMetrics.fleetConstruction.duration = Number(process.hrtime.bigint() - fleetConstructionStart) / 1000000; - } catch (error) { - systemMetrics.fleetConstruction.error = error.message; - logger.error('Fleet construction processing failed', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - // Continue processing other systems even if this fails - } + // Process resource production + await this.processResourceProduction(playerId, tickNumber); + + // Process building construction + await this.processBuildingConstruction(playerId, tickNumber); + + // Process research + await this.processResearch(playerId, tickNumber); + + // Process fleet movements + await this.processFleetMovements(playerId, tickNumber); // Update player last tick processed await db('players') .where('id', playerId) .update({ last_tick_processed: tickNumber }); - const totalDuration = Number(process.hrtime.bigint() - startTime) / 1000000; - - // Log performance metrics if processing took too long - if (totalDuration > 1000) { // More than 1 second - logger.warn('Slow player tick processing detected', { - playerId, - tickNumber, - totalDuration: `${totalDuration.toFixed(2)}ms`, - systemMetrics - }); - } - - return { - totalResourcesProduced, - playerId, - tickNumber, - success: true, - systemMetrics, - totalDuration - }; - } finally { await redisClient.lock.release(lockKey, lockToken); } } catch (error) { - const totalDuration = Number(process.hrtime.bigint() - startTime) / 1000000; logger.error('Player tick processing failed', { playerId, tickNumber, error: error.message, - systemMetrics, - totalDuration: `${totalDuration.toFixed(2)}ms` }); throw error; } @@ -526,32 +254,8 @@ class GameTickService { * @param {number} tickNumber - Current tick number */ async processResourceProduction(playerId, tickNumber) { - try { - const ResourceService = require('./resource/ResourceService'); - const serviceLocator = require('./ServiceLocator'); - const gameEventService = serviceLocator.get('gameEventService'); - const resourceService = new ResourceService(gameEventService); - - // Process production for this specific player's colonies - const result = await this.processPlayerResourceProduction(playerId, tickNumber, resourceService); - - logger.debug('Resource production processed for player', { - playerId, - tickNumber, - resourcesProduced: result.totalResourcesProduced, - coloniesProcessed: result.processedColonies, - }); - - return result; - } catch (error) { - logger.error('Failed to process resource production for player', { - playerId, - tickNumber, - error: error.message, - stack: error.stack, - }); - throw error; - } + // TODO: Implement resource production logic + logger.debug('Processing resource production', { playerId, tickNumber }); } /** @@ -560,84 +264,8 @@ class GameTickService { * @param {number} tickNumber - Current tick number */ async processBuildingConstruction(playerId, tickNumber) { - try { - const now = new Date(); - - // Get buildings under construction that should be completed - const completingBuildings = await db('colony_buildings') - .select([ - 'colony_buildings.*', - 'colonies.player_id', - 'colonies.name as colony_name', - 'building_types.name as building_name' - ]) - .join('colonies', 'colony_buildings.colony_id', 'colonies.id') - .join('building_types', 'colony_buildings.building_type_id', 'building_types.id') - .where('colonies.player_id', playerId) - .where('colony_buildings.status', 'under_construction') - .where('colony_buildings.completion_time', '<=', now); - - if (completingBuildings.length === 0) { - return null; - } - - const serviceLocator = require('./ServiceLocator'); - const gameEventService = serviceLocator.get('gameEventService'); - const completed = []; - - for (const building of completingBuildings) { - // Complete the building - await db('colony_buildings') - .where('id', building.id) - .update({ - status: 'operational', - completion_time: null, - last_updated: now - }); - - completed.push({ - buildingId: building.id, - colonyId: building.colony_id, - colonyName: building.colony_name, - buildingName: building.building_name, - level: building.level - }); - - // Emit WebSocket event - if (gameEventService) { - gameEventService.emitBuildingConstructed( - playerId, - building.colony_id, - { - id: building.id, - building_type_id: building.building_type_id, - level: building.level, - created_at: now.toISOString() - }, - `tick-${tickNumber}-building-completion` - ); - } - - logger.info('Building construction completed', { - playerId, - tickNumber, - buildingId: building.id, - colonyId: building.colony_id, - buildingName: building.building_name - }); - } - - return completed; - - } catch (error) { - logger.error('Failed to process building construction', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - throw error; - } + // TODO: Implement building construction logic + logger.debug('Processing building construction', { playerId, tickNumber }); } /** @@ -646,59 +274,8 @@ class GameTickService { * @param {number} tickNumber - Current tick number */ async processResearch(playerId, tickNumber) { - try { - const ResearchService = require('./research/ResearchService'); - const serviceLocator = require('./ServiceLocator'); - const gameEventService = serviceLocator.get('gameEventService'); - const researchService = new ResearchService(gameEventService); - - // Process research progress for this player - const result = await researchService.processResearchProgress(playerId, tickNumber); - - if (result) { - if (result.progress_updated) { - // Emit WebSocket event for research progress - if (gameEventService) { - gameEventService.emitResearchProgress( - playerId, - { - technology_id: result.technology_id, - progress: result.progress, - total_time: result.total_time, - completion_percentage: result.completion_percentage - }, - `tick-${tickNumber}-research-progress` - ); - } - - logger.debug('Research progress updated', { - playerId, - tickNumber, - technologyId: result.technology_id, - progress: result.progress, - completionPercentage: result.completion_percentage - }); - } else if (result.technology) { - logger.info('Research completed via game tick', { - playerId, - tickNumber, - technologyId: result.technology.id, - technologyName: result.technology.name - }); - } - } - - return result; - - } catch (error) { - logger.error('Failed to process research for player', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - throw error; - } + // TODO: Implement research progress logic + logger.debug('Processing research', { playerId, tickNumber }); } /** @@ -707,50 +284,8 @@ class GameTickService { * @param {number} tickNumber - Current tick number */ async processFleetMovements(playerId, tickNumber) { - try { - const serviceLocator = require('./ServiceLocator'); - const fleetService = serviceLocator.get('fleetService'); - - if (!fleetService) { - logger.debug('Fleet service not available, skipping fleet movement processing', { - playerId, - tickNumber - }); - return null; - } - - // Process fleet movements for this player - const result = await fleetService.processFleetMovements(playerId, tickNumber); - - if (result && result.length > 0) { - logger.info('Fleet movements processed', { - playerId, - tickNumber, - completedMovements: result.length, - fleets: result.map(movement => ({ - fleetId: movement.fleet_id, - fleetName: movement.fleet_name, - destination: movement.arrived_at - })) - }); - } else { - logger.debug('No fleet movements to process', { - playerId, - tickNumber - }); - } - - return result; - - } catch (error) { - logger.error('Failed to process fleet movements for player', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - throw error; - } + // TODO: Implement fleet movement logic + logger.debug('Processing fleet movements', { playerId, tickNumber }); } /** @@ -781,7 +316,7 @@ class GameTickService { */ async logTickComplete(logId, processedPlayers) { const completedAt = new Date(); - + await db('game_tick_log') .where('id', logId) .update({ @@ -819,7 +354,7 @@ class GameTickService { */ async applyBonusTick(tickNumber, userGroup) { logger.info('Applying bonus tick', { tickNumber, userGroup }); - + await db('game_tick_log') .where('tick_number', tickNumber) .where('user_group', userGroup) @@ -828,274 +363,6 @@ class GameTickService { // TODO: Implement actual bonus tick logic } - /** - * Process resource production for a specific player - * @param {number} playerId - Player ID - * @param {number} tickNumber - Current tick number - * @param {ResourceService} resourceService - Resource service instance - * @returns {Promise} Production result - */ - async processPlayerResourceProduction(playerId, tickNumber, resourceService) { - try { - let totalResourcesProduced = 0; - let processedColonies = 0; - - // Get all player colonies with production - const productionEntries = await db('colony_resource_production') - .select([ - 'colony_resource_production.*', - 'colonies.player_id', - 'resource_types.name as resource_name', - ]) - .join('colonies', 'colony_resource_production.colony_id', 'colonies.id') - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colonies.player_id', playerId) - .where('colony_resource_production.production_rate', '>', 0) - .where('resource_types.is_active', true); - - if (productionEntries.length === 0) { - return { totalResourcesProduced: 0, processedColonies: 0 }; - } - - // Process production in transaction - await db.transaction(async (trx) => { - const resourceUpdates = {}; - - for (const entry of productionEntries) { - // Calculate production since last update - const timeSinceLastUpdate = new Date() - new Date(entry.last_calculated || entry.created_at); - const hoursElapsed = Math.max(timeSinceLastUpdate / (1000 * 60 * 60), 0.1); // Minimum 0.1 hours - const productionAmount = Math.max(Math.floor(entry.production_rate * hoursElapsed), 1); - - if (productionAmount > 0) { - // Update colony storage - await trx('colony_resource_production') - .where('id', entry.id) - .increment('current_stored', productionAmount) - .update('last_calculated', new Date()); - - // Add to player resources - if (!resourceUpdates[entry.resource_name]) { - resourceUpdates[entry.resource_name] = 0; - } - resourceUpdates[entry.resource_name] += productionAmount; - totalResourcesProduced += productionAmount; - } - } - - // Add resources to player stockpile - if (Object.keys(resourceUpdates).length > 0) { - const correlationId = `tick-${tickNumber}-player-${playerId}`; - await resourceService.addPlayerResources(playerId, resourceUpdates, correlationId, trx); - - // Emit WebSocket event for resource updates - if (resourceService.gameEventService) { - resourceService.gameEventService.emitResourcesUpdated( - playerId, - resourceUpdates, - 'production', - correlationId, - ); - } - } - - processedColonies = productionEntries.length; - }); - - return { - totalResourcesProduced, - processedColonies, - playerId, - tickNumber, - }; - - } catch (error) { - logger.error('Failed to process player resource production', { - playerId, - tickNumber, - error: error.message, - stack: error.stack, - }); - throw error; - } - } - - /** - * Process fleet construction - * @param {number} playerId - Player ID - * @param {number} tickNumber - Current tick number - */ - async processFleetConstruction(playerId, tickNumber) { - try { - const now = new Date(); - - // Get fleets under construction that should be completed - // For simplicity, we'll assume construction takes 5 minutes from creation - const constructionTimeMinutes = 5; - const completionThreshold = new Date(now.getTime() - (constructionTimeMinutes * 60 * 1000)); - - const completingFleets = await db('fleets') - .select('*') - .where('player_id', playerId) - .where('fleet_status', 'constructing') - .where('created_at', '<=', completionThreshold); - - if (completingFleets.length === 0) { - return []; - } - - const serviceLocator = require('./ServiceLocator'); - const gameEventService = serviceLocator.get('gameEventService'); - const completedConstruction = []; - - for (const fleet of completingFleets) { - // Complete fleet construction - await db('fleets') - .where('id', fleet.id) - .update({ - fleet_status: 'idle', - last_updated: now - }); - - const shipsConstructed = await this.getFleetShipCount(fleet.id); - - const constructionResult = { - fleet_id: fleet.id, - fleet_name: fleet.name, - location: fleet.current_location, - ships_constructed: shipsConstructed, - construction_time: constructionTimeMinutes - }; - - completedConstruction.push(constructionResult); - - // Emit WebSocket event - if (gameEventService) { - gameEventService.emitFleetConstructionCompleted( - playerId, - constructionResult, - `tick-${tickNumber}-fleet-construction` - ); - } - - logger.info('Fleet construction completed', { - playerId, - tickNumber, - fleetId: fleet.id, - fleetName: fleet.name, - location: fleet.current_location, - shipsConstructed: shipsConstructed - }); - } - - return completedConstruction; - - } catch (error) { - logger.error('Failed to process fleet construction for player', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Aggregate system metrics from multiple user groups - * @param {Object} globalMetrics - Global metrics object to aggregate into - * @param {Object} groupMetrics - Group metrics to aggregate from - */ - aggregateSystemMetrics(globalMetrics, groupMetrics) { - try { - Object.keys(globalMetrics).forEach(systemName => { - if (groupMetrics[systemName]) { - const global = globalMetrics[systemName]; - const group = groupMetrics[systemName]; - - // Aggregate totals - global.totalProcessed += group.processed ? 1 : 0; - global.totalErrors += group.error ? 1 : 0; - - // Calculate average duration - if (group.duration > 0) { - if (global.avgDuration === 0) { - global.avgDuration = group.duration; - } else { - // Running average calculation - const totalSuccessful = global.totalProcessed - global.totalErrors; - if (totalSuccessful > 0) { - global.avgDuration = ((global.avgDuration * (totalSuccessful - 1)) + group.duration) / totalSuccessful; - } - } - } - } - }); - } catch (error) { - logger.error('Failed to aggregate system metrics', { - error: error.message, - globalMetrics, - groupMetrics - }); - } - } - - /** - * Validate cross-system resource dependencies before processing - * @param {number} playerId - Player ID - * @param {number} tickNumber - Current tick number - * @returns {Promise} Validation result - */ - async validateCrossSystemDependencies(playerId, tickNumber) { - try { - // Get current player resources - const playerResources = await db('player_resources') - .select([ - 'resource_types.name', - 'player_resources.amount' - ]) - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId); - - const resourceMap = new Map(); - playerResources.forEach(resource => { - resourceMap.set(resource.name, resource.amount); - }); - - // Check for any ongoing research that might consume resources - const ongoingResearch = await db('player_research') - .select(['technology_id', 'status']) - .where('player_id', playerId) - .where('status', 'researching'); - - // Check for fleet construction that might need resources - const constructingFleets = await db('fleets') - .select(['id', 'name', 'fleet_status']) - .where('player_id', playerId) - .where('fleet_status', 'constructing'); - - return { - valid: true, - playerResources: resourceMap, - ongoingResearch: ongoingResearch.length > 0, - constructingFleets: constructingFleets.length, - tickNumber - }; - - } catch (error) { - logger.error('Failed to validate cross-system dependencies', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - return { - valid: false, - error: error.message - }; - } - } - /** * Utility sleep function * @param {number} ms - Milliseconds to sleep @@ -1142,4 +409,4 @@ async function initializeGameTick() { module.exports = { gameTickService, initializeGameTick, -}; +}; \ No newline at end of file diff --git a/src/services/research/ResearchService.js b/src/services/research/ResearchService.js deleted file mode 100644 index 9463286..0000000 --- a/src/services/research/ResearchService.js +++ /dev/null @@ -1,729 +0,0 @@ -/** - * Research Service - * Handles all research-related operations including technology trees, - * research progress, and research completion - */ - -const logger = require('../../utils/logger'); -const db = require('../../database/connection'); -const { - TECHNOLOGIES, - getTechnologyById, - getAvailableTechnologies, - validateTechnologyResearch, - calculateResearchBonuses -} = require('../../data/technologies'); - -class ResearchService { - constructor(gameEventService = null) { - this.gameEventService = gameEventService; - } - - /** - * Get all available technologies for a player - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Available technologies - */ - async getAvailableTechnologies(playerId, correlationId) { - try { - logger.info('Getting available technologies for player', { - correlationId, - playerId - }); - - // Get completed technologies for this player - const completedTechs = await db('player_research') - .select('technology_id') - .where('player_id', playerId) - .where('status', 'completed'); - - const completedTechIds = completedTechs.map(tech => tech.technology_id); - - // Get available technologies based on prerequisites - const availableTechs = getAvailableTechnologies(completedTechIds); - - // Get current research status for available techs - const currentResearch = await db('player_research') - .select('technology_id', 'status', 'progress', 'started_at') - .where('player_id', playerId) - .whereIn('status', ['available', 'researching']); - - const researchStatusMap = new Map(); - currentResearch.forEach(research => { - researchStatusMap.set(research.technology_id, research); - }); - - // Combine technology data with research status - const result = availableTechs.map(tech => { - const status = researchStatusMap.get(tech.id); - return { - ...tech, - research_status: status ? status.status : 'unavailable', - progress: status ? status.progress : 0, - started_at: status ? status.started_at : null - }; - }); - - logger.debug('Available technologies retrieved', { - correlationId, - playerId, - availableCount: result.length, - completedCount: completedTechIds.length - }); - - return result; - - } catch (error) { - logger.error('Failed to get available technologies', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Get current research status for a player - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Research status - */ - async getResearchStatus(playerId, correlationId) { - try { - logger.info('Getting research status for player', { - correlationId, - playerId - }); - - // Get current research - const currentResearch = await db('player_research') - .select([ - 'player_research.*', - 'technologies.name', - 'technologies.description', - 'technologies.category', - 'technologies.tier', - 'technologies.research_time' - ]) - .join('technologies', 'player_research.technology_id', 'technologies.id') - .where('player_research.player_id', playerId) - .where('player_research.status', 'researching') - .first(); - - // Get completed research count - const completedCount = await db('player_research') - .count('* as count') - .where('player_id', playerId) - .where('status', 'completed') - .first(); - - // Get available research count - const availableTechs = await this.getAvailableTechnologies(playerId, correlationId); - const availableCount = availableTechs.filter(tech => - tech.research_status === 'available' - ).length; - - // Calculate research bonuses - const completedTechs = await db('player_research') - .select('technology_id') - .where('player_id', playerId) - .where('status', 'completed'); - - const completedTechIds = completedTechs.map(tech => tech.technology_id); - const researchBonuses = calculateResearchBonuses(completedTechIds); - - // Get research facilities - const researchFacilities = await db('research_facilities') - .select([ - 'research_facilities.*', - 'colonies.name as colony_name' - ]) - .join('colonies', 'research_facilities.colony_id', 'colonies.id') - .where('colonies.player_id', playerId) - .where('research_facilities.is_active', true); - - const result = { - current_research: currentResearch ? { - technology_id: currentResearch.technology_id, - name: currentResearch.name, - description: currentResearch.description, - category: currentResearch.category, - tier: currentResearch.tier, - progress: currentResearch.progress, - research_time: currentResearch.research_time, - started_at: currentResearch.started_at, - completion_percentage: (currentResearch.progress / currentResearch.research_time) * 100 - } : null, - statistics: { - completed_technologies: parseInt(completedCount.count), - available_technologies: availableCount, - research_facilities: researchFacilities.length - }, - bonuses: researchBonuses, - research_facilities: researchFacilities - }; - - logger.debug('Research status retrieved', { - correlationId, - playerId, - hasCurrentResearch: !!currentResearch, - completedCount: result.statistics.completed_technologies - }); - - return result; - - } catch (error) { - logger.error('Failed to get research status', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Start research on a technology - * @param {number} playerId - Player ID - * @param {number} technologyId - Technology ID to research - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Research start result - */ - async startResearch(playerId, technologyId, correlationId) { - try { - logger.info('Starting research for player', { - correlationId, - playerId, - technologyId - }); - - // Check if player already has research in progress - const existingResearch = await db('player_research') - .select('id', 'technology_id') - .where('player_id', playerId) - .where('status', 'researching') - .first(); - - if (existingResearch) { - const error = new Error('Player already has research in progress'); - error.statusCode = 409; - error.details = { - currentResearch: existingResearch.technology_id - }; - throw error; - } - - // Get completed technologies for validation - const completedTechs = await db('player_research') - .select('technology_id') - .where('player_id', playerId) - .where('status', 'completed'); - - const completedTechIds = completedTechs.map(tech => tech.technology_id); - - // Validate if technology can be researched - const validation = validateTechnologyResearch(technologyId, completedTechIds); - if (!validation.valid) { - const error = new Error(validation.error); - error.statusCode = 400; - error.details = validation; - throw error; - } - - const technology = validation.technology; - - // Get player resources to validate cost - const playerResources = await db('player_resources') - .select([ - 'resource_types.name', - 'player_resources.amount' - ]) - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId); - - const resourceMap = new Map(); - playerResources.forEach(resource => { - resourceMap.set(resource.name, resource.amount); - }); - - // Validate resource costs - const insufficientResources = []; - Object.entries(technology.research_cost).forEach(([resourceName, cost]) => { - const available = resourceMap.get(resourceName) || 0; - if (available < cost) { - insufficientResources.push({ - resource: resourceName, - required: cost, - available: available, - missing: cost - available - }); - } - }); - - if (insufficientResources.length > 0) { - const error = new Error('Insufficient resources for research'); - error.statusCode = 400; - error.details = { - insufficientResources - }; - throw error; - } - - // Start research in transaction - const result = await db.transaction(async (trx) => { - // Deduct research costs - for (const [resourceName, cost] of Object.entries(technology.research_cost)) { - await trx('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId) - .where('resource_types.name', resourceName) - .decrement('amount', cost); - } - - // Create or update player research record - const existingRecord = await trx('player_research') - .select('id') - .where('player_id', playerId) - .where('technology_id', technologyId) - .first(); - - if (existingRecord) { - // Update existing record - await trx('player_research') - .where('id', existingRecord.id) - .update({ - status: 'researching', - progress: 0, - started_at: new Date() - }); - } else { - // Create new record - await trx('player_research') - .insert({ - player_id: playerId, - technology_id: technologyId, - status: 'researching', - progress: 0, - started_at: new Date() - }); - } - - return { - technology_id: technologyId, - name: technology.name, - description: technology.description, - category: technology.category, - tier: technology.tier, - research_time: technology.research_time, - costs_paid: technology.research_cost, - started_at: new Date().toISOString() - }; - }); - - // Emit WebSocket event for resource deduction - if (this.gameEventService) { - const resourceChanges = {}; - Object.entries(technology.research_cost).forEach(([resourceName, cost]) => { - resourceChanges[resourceName] = -cost; - }); - - this.gameEventService.emitResourcesUpdated( - playerId, - resourceChanges, - 'research_started', - correlationId - ); - - // Emit research started event - this.gameEventService.emitResearchStarted( - playerId, - result, - correlationId - ); - } - - logger.info('Research started successfully', { - correlationId, - playerId, - technologyId, - technologyName: technology.name, - researchTime: technology.research_time - }); - - return result; - - } catch (error) { - logger.error('Failed to start research', { - correlationId, - playerId, - technologyId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Cancel current research - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Cancellation result - */ - async cancelResearch(playerId, correlationId) { - try { - logger.info('Cancelling research for player', { - correlationId, - playerId - }); - - // Get current research - const currentResearch = await db('player_research') - .select([ - 'player_research.*', - 'technologies.name', - 'technologies.research_cost' - ]) - .join('technologies', 'player_research.technology_id', 'technologies.id') - .where('player_research.player_id', playerId) - .where('player_research.status', 'researching') - .first(); - - if (!currentResearch) { - const error = new Error('No research in progress to cancel'); - error.statusCode = 400; - throw error; - } - - // Calculate partial refund based on progress (50% of remaining cost) - const progressPercentage = currentResearch.progress / (currentResearch.research_time || 1); - const refundPercentage = Math.max(0, (1 - progressPercentage) * 0.5); - - const researchCost = JSON.parse(currentResearch.research_cost); - const refundAmounts = {}; - - Object.entries(researchCost).forEach(([resourceName, cost]) => { - refundAmounts[resourceName] = Math.floor(cost * refundPercentage); - }); - - // Cancel research in transaction - const result = await db.transaction(async (trx) => { - // Update research status - await trx('player_research') - .where('id', currentResearch.id) - .update({ - status: 'available', - progress: 0, - started_at: null - }); - - // Refund partial resources - for (const [resourceName, refundAmount] of Object.entries(refundAmounts)) { - if (refundAmount > 0) { - await trx('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId) - .where('resource_types.name', resourceName) - .increment('amount', refundAmount); - } - } - - return { - cancelled_technology: { - id: currentResearch.technology_id, - name: currentResearch.name, - progress: currentResearch.progress, - progress_percentage: progressPercentage * 100 - }, - refund: refundAmounts, - refund_percentage: refundPercentage * 100 - }; - }); - - // Emit WebSocket events - if (this.gameEventService) { - // Emit resource refund - this.gameEventService.emitResourcesUpdated( - playerId, - refundAmounts, - 'research_cancelled', - correlationId - ); - - // Emit research cancelled event - this.gameEventService.emitResearchCancelled( - playerId, - result.cancelled_technology, - correlationId - ); - } - - logger.info('Research cancelled successfully', { - correlationId, - playerId, - technologyId: currentResearch.technology_id, - progressLost: currentResearch.progress, - refundAmount: Object.values(refundAmounts).reduce((sum, amount) => sum + amount, 0) - }); - - return result; - - } catch (error) { - logger.error('Failed to cancel research', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Process research progress for a player (called from game tick) - * @param {number} playerId - Player ID - * @param {number} tickNumber - Current tick number - * @returns {Promise} Completion result if research completed - */ - async processResearchProgress(playerId, tickNumber) { - try { - // Get current research - const currentResearch = await db('player_research') - .select([ - 'player_research.*', - 'technologies.name', - 'technologies.description', - 'technologies.research_time', - 'technologies.effects', - 'technologies.unlocks' - ]) - .join('technologies', 'player_research.technology_id', 'technologies.id') - .where('player_research.player_id', playerId) - .where('player_research.status', 'researching') - .first(); - - if (!currentResearch) { - return null; // No research in progress - } - - // Calculate research bonuses from completed technologies - const completedTechs = await db('player_research') - .select('technology_id') - .where('player_id', playerId) - .where('status', 'completed'); - - const completedTechIds = completedTechs.map(tech => tech.technology_id); - const bonuses = calculateResearchBonuses(completedTechIds); - - // Calculate research facilities bonus - const researchFacilities = await db('research_facilities') - .select(['research_bonus', 'specialization']) - .join('colonies', 'research_facilities.colony_id', 'colonies.id') - .where('colonies.player_id', playerId) - .where('research_facilities.is_active', true); - - let facilityBonus = 0; - researchFacilities.forEach(facility => { - facilityBonus += facility.research_bonus || 0; - }); - - // Calculate total research speed multiplier - const baseSpeedMultiplier = 1.0; - const technologySpeedBonus = bonuses.research_speed_bonus || 0; - const facilitySpeedBonus = facilityBonus; - const totalSpeedMultiplier = baseSpeedMultiplier + technologySpeedBonus + facilitySpeedBonus; - - // Calculate progress increment (assuming 1 minute per tick as base) - const progressIncrement = Math.max(1, Math.floor(1 * totalSpeedMultiplier)); - const newProgress = currentResearch.progress + progressIncrement; - - // Check if research is completed - if (newProgress >= currentResearch.research_time) { - // Complete the research - const completionResult = await this.completeResearch( - playerId, - currentResearch, - `tick-${tickNumber}-research-completion` - ); - - logger.info('Research completed via game tick', { - playerId, - tickNumber, - technologyId: currentResearch.technology_id, - technologyName: currentResearch.name, - totalTime: currentResearch.research_time, - speedMultiplier: totalSpeedMultiplier - }); - - return completionResult; - } else { - // Update progress - await db('player_research') - .where('id', currentResearch.id) - .update({ progress: newProgress }); - - logger.debug('Research progress updated', { - playerId, - tickNumber, - technologyId: currentResearch.technology_id, - progress: newProgress, - totalTime: currentResearch.research_time, - progressPercentage: (newProgress / currentResearch.research_time) * 100 - }); - - return { - progress_updated: true, - technology_id: currentResearch.technology_id, - progress: newProgress, - total_time: currentResearch.research_time, - completion_percentage: (newProgress / currentResearch.research_time) * 100 - }; - } - - } catch (error) { - logger.error('Failed to process research progress', { - playerId, - tickNumber, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Complete research for a technology - * @param {number} playerId - Player ID - * @param {Object} researchData - Research data - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Completion result - */ - async completeResearch(playerId, researchData, correlationId) { - try { - const completionResult = await db.transaction(async (trx) => { - // Mark research as completed - await trx('player_research') - .where('id', researchData.id) - .update({ - status: 'completed', - progress: researchData.research_time, - completed_at: new Date() - }); - - // Parse effects and unlocks - const effects = typeof researchData.effects === 'string' - ? JSON.parse(researchData.effects) - : researchData.effects || {}; - - const unlocks = typeof researchData.unlocks === 'string' - ? JSON.parse(researchData.unlocks) - : researchData.unlocks || {}; - - return { - technology: { - id: researchData.technology_id, - name: researchData.name, - description: researchData.description, - effects: effects, - unlocks: unlocks - }, - completed_at: new Date().toISOString(), - research_time: researchData.research_time - }; - }); - - // Emit WebSocket event for research completion - if (this.gameEventService) { - this.gameEventService.emitResearchCompleted( - playerId, - completionResult, - correlationId - ); - } - - logger.info('Research completed successfully', { - correlationId, - playerId, - technologyId: researchData.technology_id, - technologyName: researchData.name - }); - - return completionResult; - - } catch (error) { - logger.error('Failed to complete research', { - correlationId, - playerId, - technologyId: researchData.technology_id, - error: error.message, - stack: error.stack - }); - throw error; - } - } - - /** - * Get completed technologies for a player - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Completed technologies - */ - async getCompletedTechnologies(playerId, correlationId) { - try { - logger.info('Getting completed technologies for player', { - correlationId, - playerId - }); - - const completedTechs = await db('player_research') - .select([ - 'player_research.technology_id', - 'player_research.completed_at', - 'technologies.name', - 'technologies.description', - 'technologies.category', - 'technologies.tier', - 'technologies.effects', - 'technologies.unlocks' - ]) - .join('technologies', 'player_research.technology_id', 'technologies.id') - .where('player_research.player_id', playerId) - .where('player_research.status', 'completed') - .orderBy('player_research.completed_at', 'desc'); - - const result = completedTechs.map(tech => ({ - id: tech.technology_id, - name: tech.name, - description: tech.description, - category: tech.category, - tier: tech.tier, - effects: typeof tech.effects === 'string' ? JSON.parse(tech.effects) : tech.effects, - unlocks: typeof tech.unlocks === 'string' ? JSON.parse(tech.unlocks) : tech.unlocks, - completed_at: tech.completed_at - })); - - logger.debug('Completed technologies retrieved', { - correlationId, - playerId, - count: result.length - }); - - return result; - - } catch (error) { - logger.error('Failed to get completed technologies', { - correlationId, - playerId, - error: error.message, - stack: error.stack - }); - throw error; - } - } -} - -module.exports = ResearchService; \ No newline at end of file diff --git a/src/services/resource/ResourceService.js b/src/services/resource/ResourceService.js deleted file mode 100644 index 6f59a77..0000000 --- a/src/services/resource/ResourceService.js +++ /dev/null @@ -1,600 +0,0 @@ -/** - * Resource Service - * Handles all resource-related business logic including player resources, production calculations, and transfers - */ - -const db = require('../../database/connection'); -const logger = require('../../utils/logger'); -const { ValidationError, NotFoundError, ServiceError } = require('../../middleware/error.middleware'); - -class ResourceService { - constructor(gameEventService = null) { - this.gameEventService = gameEventService; - } - /** - * Initialize player resources when they register - * @param {number} playerId - Player ID - * @param {Object} trx - Database transaction - * @returns {Promise} - */ - async initializePlayerResources(playerId, trx) { - try { - logger.info('Initializing player resources', { playerId }); - - // Get all active resource types - const resourceTypes = await trx('resource_types') - .where('is_active', true) - .orderBy('id'); - - // Starting resources configuration - const startingResources = { - scrap: parseInt(process.env.STARTING_RESOURCES_SCRAP) || 1000, - energy: parseInt(process.env.STARTING_RESOURCES_ENERGY) || 500, - data_cores: 0, - rare_elements: 0, - }; - - // Create player resource entries - const resourceEntries = resourceTypes.map(resourceType => ({ - player_id: playerId, - resource_type_id: resourceType.id, - amount: startingResources[resourceType.name] || 0, - storage_capacity: null, // Unlimited by default - last_updated: new Date(), - })); - - await trx('player_resources').insert(resourceEntries); - - logger.info('Player resources initialized successfully', { - playerId, - resourceCount: resourceEntries.length, - }); - - } catch (error) { - logger.error('Failed to initialize player resources', { - playerId, - error: error.message, - stack: error.stack, - }); - throw new ServiceError('Failed to initialize player resources', error); - } - } - - /** - * Get player resources - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Player resource data - */ - async getPlayerResources(playerId, correlationId) { - try { - logger.info('Fetching player resources', { - correlationId, - playerId, - }); - - const resources = await db('player_resources') - .select([ - 'player_resources.*', - 'resource_types.name as resource_name', - 'resource_types.description', - 'resource_types.category', - 'resource_types.max_storage as type_max_storage', - 'resource_types.decay_rate', - 'resource_types.trade_value', - 'resource_types.is_tradeable', - ]) - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId) - .where('resource_types.is_active', true) - .orderBy('resource_types.category') - .orderBy('resource_types.name'); - - logger.info('Player resources retrieved', { - correlationId, - playerId, - resourceCount: resources.length, - }); - - return resources; - - } catch (error) { - logger.error('Failed to fetch player resources', { - correlationId, - playerId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve player resources', error); - } - } - - /** - * Get player resource summary (simplified view) - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Resource summary - */ - async getPlayerResourceSummary(playerId, correlationId) { - try { - logger.info('Fetching player resource summary', { - correlationId, - playerId, - }); - - const resources = await this.getPlayerResources(playerId, correlationId); - - const summary = {}; - resources.forEach(resource => { - summary[resource.resource_name] = { - amount: parseInt(resource.amount) || 0, - category: resource.category, - storageCapacity: resource.storage_capacity, - isAtCapacity: resource.storage_capacity && resource.amount >= resource.storage_capacity, - }; - }); - - logger.info('Player resource summary retrieved', { - correlationId, - playerId, - resourceTypes: Object.keys(summary), - }); - - return summary; - - } catch (error) { - logger.error('Failed to fetch player resource summary', { - correlationId, - playerId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve player resource summary', error); - } - } - - /** - * Calculate total resource production across all player colonies - * @param {number} playerId - Player ID - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Production rates by resource type - */ - async calculatePlayerResourceProduction(playerId, correlationId) { - try { - logger.info('Calculating player resource production', { - correlationId, - playerId, - }); - - // Get all player colonies with their resource production - const productionData = await db('colony_resource_production') - .select([ - 'resource_types.name as resource_name', - db.raw('SUM(colony_resource_production.production_rate) as total_production'), - db.raw('SUM(colony_resource_production.consumption_rate) as total_consumption'), - db.raw('SUM(colony_resource_production.current_stored) as total_stored'), - ]) - .join('colonies', 'colony_resource_production.colony_id', 'colonies.id') - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colonies.player_id', playerId) - .where('resource_types.is_active', true) - .groupBy('resource_types.id', 'resource_types.name') - .orderBy('resource_types.name'); - - const productionSummary = {}; - productionData.forEach(data => { - const netProduction = parseInt(data.total_production) - parseInt(data.total_consumption); - productionSummary[data.resource_name] = { - production: parseInt(data.total_production) || 0, - consumption: parseInt(data.total_consumption) || 0, - netProduction, - storedInColonies: parseInt(data.total_stored) || 0, - }; - }); - - logger.info('Player resource production calculated', { - correlationId, - playerId, - productionSummary, - }); - - return productionSummary; - - } catch (error) { - logger.error('Failed to calculate player resource production', { - correlationId, - playerId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to calculate resource production', error); - } - } - - /** - * Add resources to a player's stockpile - * @param {number} playerId - Player ID - * @param {Object} resources - Resources to add (resourceName: amount) - * @param {string} correlationId - Request correlation ID - * @param {Object} trx - Optional database transaction - * @returns {Promise} Updated resource amounts - */ - async addPlayerResources(playerId, resources, correlationId, trx = null) { - try { - logger.info('Adding resources to player', { - correlationId, - playerId, - resources, - }); - - const dbContext = trx || db; - const updatedResources = {}; - - for (const [resourceName, amount] of Object.entries(resources)) { - if (amount <= 0) continue; - - // Update resource amount - const result = await dbContext('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId) - .where('resource_types.name', resourceName) - .increment('player_resources.amount', amount) - .update('player_resources.last_updated', new Date()) - .returning(['player_resources.amount', 'resource_types.name']); - - if (result.length > 0) { - updatedResources[resourceName] = parseInt(result[0].amount); - } - } - - logger.info('Resources added successfully', { - correlationId, - playerId, - updatedResources, - }); - - // Emit WebSocket event for resource update - if (this.gameEventService) { - this.gameEventService.emitResourcesUpdated(playerId, updatedResources, 'admin_addition', correlationId); - } - - return updatedResources; - - } catch (error) { - logger.error('Failed to add player resources', { - correlationId, - playerId, - resources, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to add player resources', error); - } - } - - /** - * Deduct resources from a player's stockpile - * @param {number} playerId - Player ID - * @param {Object} resources - Resources to deduct (resourceName: amount) - * @param {string} correlationId - Request correlation ID - * @param {Object} trx - Optional database transaction - * @returns {Promise} Updated resource amounts - */ - async deductPlayerResources(playerId, resources, correlationId, trx = null) { - try { - logger.info('Deducting resources from player', { - correlationId, - playerId, - resources, - }); - - const dbContext = trx || db; - - // First check if player has enough resources - const canAfford = await this.checkResourceAffordability(playerId, resources, correlationId, dbContext); - if (!canAfford.canAfford) { - throw new ValidationError('Insufficient resources', { missing: canAfford.missing }); - } - - const updatedResources = {}; - - for (const [resourceName, amount] of Object.entries(resources)) { - if (amount <= 0) continue; - - // Deduct resource amount - const result = await dbContext('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId) - .where('resource_types.name', resourceName) - .decrement('player_resources.amount', amount) - .update('player_resources.last_updated', new Date()) - .returning(['player_resources.amount', 'resource_types.name']); - - if (result.length > 0) { - updatedResources[resourceName] = parseInt(result[0].amount); - } - } - - logger.info('Resources deducted successfully', { - correlationId, - playerId, - updatedResources, - }); - - // Emit WebSocket event for resource update - if (this.gameEventService) { - this.gameEventService.emitResourcesUpdated(playerId, updatedResources, 'resource_spent', correlationId); - } - - return updatedResources; - - } catch (error) { - logger.error('Failed to deduct player resources', { - correlationId, - playerId, - resources, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError) { - throw error; - } - throw new ServiceError('Failed to deduct player resources', error); - } - } - - /** - * Check if player can afford specific resource costs - * @param {number} playerId - Player ID - * @param {Object} costs - Resource costs to check - * @param {string} correlationId - Request correlation ID - * @param {Object} dbContext - Database context (for transactions) - * @returns {Promise} Affordability result - */ - async checkResourceAffordability(playerId, costs, correlationId, dbContext = db) { - try { - const result = { canAfford: true, missing: {} }; - - // Get player resources - const playerResources = await dbContext('player_resources') - .select([ - 'player_resources.amount', - 'resource_types.name as resource_name', - ]) - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', playerId); - - const resourceMap = {}; - playerResources.forEach(resource => { - resourceMap[resource.resource_name] = parseInt(resource.amount); - }); - - // Check each cost requirement - for (const [resourceName, requiredAmount] of Object.entries(costs)) { - const available = resourceMap[resourceName] || 0; - if (available < requiredAmount) { - result.canAfford = false; - result.missing[resourceName] = requiredAmount - available; - } - } - - return result; - - } catch (error) { - logger.error('Failed to check resource affordability', { - correlationId, - playerId, - costs, - error: error.message, - }); - - return { canAfford: false, missing: {} }; - } - } - - /** - * Transfer resources between colonies - * @param {number} fromColonyId - Source colony ID - * @param {number} toColonyId - Destination colony ID - * @param {Object} resources - Resources to transfer - * @param {number} playerId - Player ID (for authorization) - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Transfer result - */ - async transferResourcesBetweenColonies(fromColonyId, toColonyId, resources, playerId, correlationId) { - try { - logger.info('Transferring resources between colonies', { - correlationId, - fromColonyId, - toColonyId, - resources, - playerId, - }); - - // Verify both colonies belong to the player - const [fromColony, toColony] = await Promise.all([ - db('colonies').where('id', fromColonyId).where('player_id', playerId).first(), - db('colonies').where('id', toColonyId).where('player_id', playerId).first(), - ]); - - if (!fromColony || !toColony) { - throw new NotFoundError('One or both colonies not found or access denied'); - } - - // Execute transfer in transaction - const result = await db.transaction(async (trx) => { - for (const [resourceName, amount] of Object.entries(resources)) { - if (amount <= 0) continue; - - // Check if source colony has enough resources - const sourceResource = await trx('colony_resource_production') - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colony_resource_production.colony_id', fromColonyId) - .where('resource_types.name', resourceName) - .first(); - - if (!sourceResource || sourceResource.current_stored < amount) { - throw new ValidationError(`Insufficient ${resourceName} in source colony`); - } - - // Transfer resources - await trx('colony_resource_production') - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colony_resource_production.colony_id', fromColonyId) - .where('resource_types.name', resourceName) - .decrement('colony_resource_production.current_stored', amount) - .update('colony_resource_production.last_calculated', new Date()); - - await trx('colony_resource_production') - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colony_resource_production.colony_id', toColonyId) - .where('resource_types.name', resourceName) - .increment('colony_resource_production.current_stored', amount) - .update('colony_resource_production.last_calculated', new Date()); - } - - return { success: true, transferred: resources }; - }); - - logger.info('Resources transferred successfully', { - correlationId, - fromColonyId, - toColonyId, - resources, - playerId, - }); - - return result; - - } catch (error) { - logger.error('Failed to transfer resources between colonies', { - correlationId, - fromColonyId, - toColonyId, - resources, - playerId, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError || error instanceof NotFoundError) { - throw error; - } - throw new ServiceError('Failed to transfer resources', error); - } - } - - /** - * Get all resource types available in the game - * @param {string} correlationId - Request correlation ID - * @returns {Promise} List of resource types - */ - async getResourceTypes(correlationId) { - try { - logger.info('Fetching resource types', { correlationId }); - - const resourceTypes = await db('resource_types') - .select('*') - .where('is_active', true) - .orderBy('category') - .orderBy('name'); - - logger.info('Resource types retrieved', { - correlationId, - count: resourceTypes.length, - }); - - return resourceTypes; - - } catch (error) { - logger.error('Failed to fetch resource types', { - correlationId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to retrieve resource types', error); - } - } - - /** - * Process resource production for all colonies (called by game tick) - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Processing result - */ - async processResourceProduction(correlationId) { - try { - logger.info('Processing resource production', { correlationId }); - - let processedColonies = 0; - let totalResourcesProduced = 0; - - // Get all active colonies with production - const productionEntries = await db('colony_resource_production') - .select([ - 'colony_resource_production.*', - 'colonies.player_id', - 'resource_types.name as resource_name', - ]) - .join('colonies', 'colony_resource_production.colony_id', 'colonies.id') - .join('resource_types', 'colony_resource_production.resource_type_id', 'resource_types.id') - .where('colony_resource_production.production_rate', '>', 0) - .where('resource_types.is_active', true); - - // Process in batches to avoid overwhelming the database - const batchSize = 50; - for (let i = 0; i < productionEntries.length; i += batchSize) { - const batch = productionEntries.slice(i, i + batchSize); - - await db.transaction(async (trx) => { - for (const entry of batch) { - // Calculate production since last update - const timeSinceLastUpdate = new Date() - new Date(entry.last_calculated); - const hoursElapsed = timeSinceLastUpdate / (1000 * 60 * 60); - const productionAmount = Math.floor(entry.production_rate * hoursElapsed); - - if (productionAmount > 0) { - // Add to colony storage - await trx('colony_resource_production') - .where('id', entry.id) - .increment('current_stored', productionAmount) - .update('last_calculated', new Date()); - - totalResourcesProduced += productionAmount; - } - } - }); - - processedColonies += batch.length; - } - - logger.info('Resource production processed', { - correlationId, - processedColonies, - totalResourcesProduced, - }); - - return { - success: true, - processedColonies, - totalResourcesProduced, - }; - - } catch (error) { - logger.error('Failed to process resource production', { - correlationId, - error: error.message, - stack: error.stack, - }); - - throw new ServiceError('Failed to process resource production', error); - } - } -} - -module.exports = ResourceService; diff --git a/src/services/user/AdminService.js b/src/services/user/AdminService.js index 7972e5c..5ffa99b 100644 --- a/src/services/user/AdminService.js +++ b/src/services/user/AdminService.js @@ -11,7 +11,7 @@ const logger = require('../../utils/logger'); const { ValidationError, ConflictError, NotFoundError, AuthenticationError, AuthorizationError } = require('../../middleware/error.middleware'); class AdminService { - /** + /** * Authenticate admin login * @param {Object} loginData - Login credentials * @param {string} loginData.email - Admin email @@ -19,163 +19,163 @@ class AdminService { * @param {string} correlationId - Request correlation ID * @returns {Promise} Authentication result with tokens */ - async authenticateAdmin(loginData, correlationId) { - try { - const { email, password } = loginData; + async authenticateAdmin(loginData, correlationId) { + try { + const { email, password } = loginData; - logger.info('Admin authentication initiated', { - correlationId, - email, - }); + logger.info('Admin authentication initiated', { + correlationId, + email + }); - // Find admin by email - const admin = await this.findAdminByEmail(email); - if (!admin) { - throw new AuthenticationError('Invalid email or password'); - } + // Find admin by email + const admin = await this.findAdminByEmail(email); + if (!admin) { + throw new AuthenticationError('Invalid email or password'); + } - // Check if admin is active - if (!admin.is_active) { - throw new AuthenticationError('Account has been deactivated'); - } + // Check if admin is active + if (!admin.is_active) { + throw new AuthenticationError('Account has been deactivated'); + } - // Verify password - const isPasswordValid = await verifyPassword(password, admin.password_hash); - if (!isPasswordValid) { - logger.warn('Admin authentication failed - invalid password', { - correlationId, - adminId: admin.id, - email: admin.email, - }); - throw new AuthenticationError('Invalid email or password'); - } + // Verify password + const isPasswordValid = await verifyPassword(password, admin.password_hash); + if (!isPasswordValid) { + logger.warn('Admin authentication failed - invalid password', { + correlationId, + adminId: admin.id, + email: admin.email + }); + throw new AuthenticationError('Invalid email or password'); + } - // Get admin permissions - const permissions = await this.getAdminPermissions(admin.id); + // Get admin permissions + const permissions = await this.getAdminPermissions(admin.id); - // Generate tokens - const accessToken = generateAdminToken({ - adminId: admin.id, - email: admin.email, - username: admin.username, - permissions, - }); + // Generate tokens + const accessToken = generateAdminToken({ + adminId: admin.id, + email: admin.email, + username: admin.username, + permissions: permissions + }); - const refreshToken = generateRefreshToken({ - userId: admin.id, - type: 'admin', - }); + const refreshToken = generateRefreshToken({ + userId: admin.id, + type: 'admin' + }); - // Update last login timestamp - await db('admins') - .where('id', admin.id) - .update({ - last_login_at: new Date(), - updated_at: new Date(), - }); + // Update last login timestamp + await db('admins') + .where('id', admin.id) + .update({ + last_login_at: new Date(), + updated_at: new Date() + }); - logger.audit('Admin authenticated successfully', { - correlationId, - adminId: admin.id, - email: admin.email, - username: admin.username, - permissions, - }); + logger.audit('Admin authenticated successfully', { + correlationId, + adminId: admin.id, + email: admin.email, + username: admin.username, + permissions: permissions + }); - return { - admin: { - id: admin.id, - email: admin.email, - username: admin.username, - permissions, - isActive: admin.is_active, - }, - tokens: { - accessToken, - refreshToken, - }, - }; + return { + admin: { + id: admin.id, + email: admin.email, + username: admin.username, + permissions: permissions, + isActive: admin.is_active + }, + tokens: { + accessToken, + refreshToken + } + }; - } catch (error) { - logger.error('Admin authentication failed', { - correlationId, - email: loginData.email, - error: error.message, - }); + } catch (error) { + logger.error('Admin authentication failed', { + correlationId, + email: loginData.email, + error: error.message + }); - if (error instanceof AuthenticationError) { - throw error; - } - throw new AuthenticationError('Authentication failed'); + if (error instanceof AuthenticationError) { + throw error; + } + throw new AuthenticationError('Authentication failed'); + } } - } - /** + /** * Get admin profile by ID * @param {number} adminId - Admin ID * @param {string} correlationId - Request correlation ID * @returns {Promise} Admin profile data */ - async getAdminProfile(adminId, correlationId) { - try { - logger.info('Fetching admin profile', { - correlationId, - adminId, - }); + async getAdminProfile(adminId, correlationId) { + try { + logger.info('Fetching admin profile', { + correlationId, + adminId + }); - const admin = await db('admins') - .select([ - 'id', - 'email', - 'username', - 'is_active', - 'created_at', - 'last_login_at', - ]) - .where('id', adminId) - .first(); + const admin = await db('admins') + .select([ + 'id', + 'email', + 'username', + 'is_active', + 'created_at', + 'last_login_at' + ]) + .where('id', adminId) + .first(); - if (!admin) { - throw new NotFoundError('Admin not found'); - } + if (!admin) { + throw new NotFoundError('Admin not found'); + } - // Get admin permissions - const permissions = await this.getAdminPermissions(adminId); + // Get admin permissions + const permissions = await this.getAdminPermissions(adminId); - const profile = { - id: admin.id, - email: admin.email, - username: admin.username, - permissions, - isActive: admin.is_active, - createdAt: admin.created_at, - lastLoginAt: admin.last_login_at, - }; + const profile = { + id: admin.id, + email: admin.email, + username: admin.username, + permissions: permissions, + isActive: admin.is_active, + createdAt: admin.created_at, + lastLoginAt: admin.last_login_at + }; - logger.info('Admin profile retrieved successfully', { - correlationId, - adminId, - username: admin.username, - }); + logger.info('Admin profile retrieved successfully', { + correlationId, + adminId, + username: admin.username + }); - return profile; + return profile; - } catch (error) { - logger.error('Failed to fetch admin profile', { - correlationId, - adminId, - error: error.message, - stack: error.stack, - }); + } catch (error) { + logger.error('Failed to fetch admin profile', { + correlationId, + adminId, + error: error.message, + stack: error.stack + }); - if (error instanceof NotFoundError) { - throw error; - } - throw new Error('Failed to retrieve admin profile'); + if (error instanceof NotFoundError) { + throw error; + } + throw new Error('Failed to retrieve admin profile'); + } } - } - /** + /** * Get players list with pagination and filtering * @param {Object} options - Query options * @param {number} options.page - Page number @@ -187,384 +187,384 @@ class AdminService { * @param {string} correlationId - Request correlation ID * @returns {Promise} Players list with pagination info */ - async getPlayersList(options, correlationId) { - try { - const { - page = 1, - limit = 20, - sortBy = 'created_at', - sortOrder = 'desc', - search = '', - activeOnly = null, - } = options; + async getPlayersList(options, correlationId) { + try { + const { + page = 1, + limit = 20, + sortBy = 'created_at', + sortOrder = 'desc', + search = '', + activeOnly = null + } = options; - logger.info('Fetching players list', { - correlationId, - page, - limit, - sortBy, - sortOrder, - search, - activeOnly, - }); + logger.info('Fetching players list', { + correlationId, + page, + limit, + sortBy, + sortOrder, + search, + activeOnly + }); - let query = db('players') - .select([ - 'id', - 'email', - 'username', - 'is_active', - 'is_verified', - 'created_at', - 'last_login_at', - ]); + let query = db('players') + .select([ + 'id', + 'email', + 'username', + 'is_active', + 'is_verified', + 'created_at', + 'last_login_at' + ]); - // Apply search filter - if (search) { - query = query.where(function () { - this.whereILike('username', `%${search}%`) - .orWhereILike('email', `%${search}%`); - }); - } + // Apply search filter + if (search) { + query = query.where(function() { + this.whereILike('username', `%${search}%`) + .orWhereILike('email', `%${search}%`); + }); + } - // Apply active filter - if (activeOnly !== null) { - query = query.where('is_active', activeOnly); - } + // Apply active filter + if (activeOnly !== null) { + query = query.where('is_active', activeOnly); + } - // Get total count - const totalQuery = query.clone(); - const totalCount = await totalQuery.count('* as count').first(); - const total = parseInt(totalCount.count); + // Get total count + const totalQuery = query.clone(); + const totalCount = await totalQuery.count('* as count').first(); + const total = parseInt(totalCount.count); - // Apply pagination and sorting - const offset = (page - 1) * limit; - const players = await query - .orderBy(sortBy, sortOrder) - .limit(limit) - .offset(offset); + // Apply pagination and sorting + const offset = (page - 1) * limit; + const players = await query + .orderBy(sortBy, sortOrder) + .limit(limit) + .offset(offset); - const result = { - players, - pagination: { - page: parseInt(page), - limit: parseInt(limit), - total, - totalPages: Math.ceil(total / limit), - hasNext: page * limit < total, - hasPrev: page > 1, - }, - }; + const result = { + players, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages: Math.ceil(total / limit), + hasNext: page * limit < total, + hasPrev: page > 1 + } + }; - logger.info('Players list retrieved successfully', { - correlationId, - playersCount: players.length, - total, - page, - }); + logger.info('Players list retrieved successfully', { + correlationId, + playersCount: players.length, + total, + page + }); - return result; + return result; - } catch (error) { - logger.error('Failed to fetch players list', { - correlationId, - error: error.message, - stack: error.stack, - }); + } catch (error) { + logger.error('Failed to fetch players list', { + correlationId, + error: error.message, + stack: error.stack + }); - throw new Error('Failed to retrieve players list'); + throw new Error('Failed to retrieve players list'); + } } - } - /** + /** * Get detailed player information for admin view * @param {number} playerId - Player ID * @param {string} correlationId - Request correlation ID * @returns {Promise} Detailed player information */ - async getPlayerDetails(playerId, correlationId) { - try { - logger.info('Fetching player details for admin', { - correlationId, - playerId, - }); + async getPlayerDetails(playerId, correlationId) { + try { + logger.info('Fetching player details for admin', { + correlationId, + playerId + }); - // Get basic player info - const player = await db('players') - .select([ - 'id', - 'email', - 'username', - 'is_active', - 'is_verified', - 'created_at', - 'updated_at', - 'last_login_at', - ]) - .where('id', playerId) - .first(); + // Get basic player info + const player = await db('players') + .select([ + 'id', + 'email', + 'username', + 'is_active', + 'is_verified', + 'created_at', + 'updated_at', + 'last_login_at' + ]) + .where('id', playerId) + .first(); - if (!player) { - throw new NotFoundError('Player not found'); - } + if (!player) { + throw new NotFoundError('Player not found'); + } - // Get player resources - const resources = await db('player_resources') - .where('player_id', playerId) - .first(); + // Get player resources + const resources = await db('player_resources') + .where('player_id', playerId) + .first(); - // Get player stats - const stats = await db('player_stats') - .where('player_id', playerId) - .first(); + // Get player stats + const stats = await db('player_stats') + .where('player_id', playerId) + .first(); - // Get colonies count - const coloniesCount = await db('colonies') - .where('player_id', playerId) - .count('* as count') - .first(); + // Get colonies count + const coloniesCount = await db('colonies') + .where('player_id', playerId) + .count('* as count') + .first(); - // Get fleets count - const fleetsCount = await db('fleets') - .where('player_id', playerId) - .count('* as count') - .first(); + // Get fleets count + const fleetsCount = await db('fleets') + .where('player_id', playerId) + .count('* as count') + .first(); - const playerDetails = { - ...player, - resources: resources || { - scrap: 0, - energy: 0, - research_points: 0, - }, - stats: stats || { - colonies_count: 0, - fleets_count: 0, - total_battles: 0, - battles_won: 0, - }, - currentCounts: { - colonies: parseInt(coloniesCount.count), - fleets: parseInt(fleetsCount.count), - }, - }; + const playerDetails = { + ...player, + resources: resources || { + scrap: 0, + energy: 0, + research_points: 0 + }, + stats: stats || { + colonies_count: 0, + fleets_count: 0, + total_battles: 0, + battles_won: 0 + }, + currentCounts: { + colonies: parseInt(coloniesCount.count), + fleets: parseInt(fleetsCount.count) + } + }; - logger.audit('Player details accessed by admin', { - correlationId, - playerId, - playerUsername: player.username, - }); + logger.audit('Player details accessed by admin', { + correlationId, + playerId, + playerUsername: player.username + }); - return playerDetails; + return playerDetails; - } catch (error) { - logger.error('Failed to fetch player details', { - correlationId, - playerId, - error: error.message, - stack: error.stack, - }); + } catch (error) { + logger.error('Failed to fetch player details', { + correlationId, + playerId, + error: error.message, + stack: error.stack + }); - if (error instanceof NotFoundError) { - throw error; - } - throw new Error('Failed to retrieve player details'); + if (error instanceof NotFoundError) { + throw error; + } + throw new Error('Failed to retrieve player details'); + } } - } - /** + /** * Update player status (activate/deactivate) * @param {number} playerId - Player ID * @param {boolean} isActive - New active status * @param {string} correlationId - Request correlation ID * @returns {Promise} Updated player data */ - async updatePlayerStatus(playerId, isActive, correlationId) { - try { - logger.info('Updating player status', { - correlationId, - playerId, - isActive, - }); + async updatePlayerStatus(playerId, isActive, correlationId) { + try { + logger.info('Updating player status', { + correlationId, + playerId, + isActive + }); - // Check if player exists - const player = await db('players') - .where('id', playerId) - .first(); + // Check if player exists + const player = await db('players') + .where('id', playerId) + .first(); - if (!player) { - throw new NotFoundError('Player not found'); - } + if (!player) { + throw new NotFoundError('Player not found'); + } - // Update player status - await db('players') - .where('id', playerId) - .update({ - is_active: isActive, - updated_at: new Date(), - }); + // Update player status + await db('players') + .where('id', playerId) + .update({ + is_active: isActive, + updated_at: new Date() + }); - const updatedPlayer = await db('players') - .select(['id', 'email', 'username', 'is_active', 'updated_at']) - .where('id', playerId) - .first(); + const updatedPlayer = await db('players') + .select(['id', 'email', 'username', 'is_active', 'updated_at']) + .where('id', playerId) + .first(); - logger.audit('Player status updated by admin', { - correlationId, - playerId, - playerUsername: player.username, - previousStatus: player.is_active, - newStatus: isActive, - }); + logger.audit('Player status updated by admin', { + correlationId, + playerId, + playerUsername: player.username, + previousStatus: player.is_active, + newStatus: isActive + }); - return updatedPlayer; + return updatedPlayer; - } catch (error) { - logger.error('Failed to update player status', { - correlationId, - playerId, - isActive, - error: error.message, - stack: error.stack, - }); + } catch (error) { + logger.error('Failed to update player status', { + correlationId, + playerId, + isActive, + error: error.message, + stack: error.stack + }); - if (error instanceof NotFoundError) { - throw error; - } - throw new Error('Failed to update player status'); + if (error instanceof NotFoundError) { + throw error; + } + throw new Error('Failed to update player status'); + } } - } - /** + /** * Get system statistics for admin dashboard * @param {string} correlationId - Request correlation ID * @returns {Promise} System statistics */ - async getSystemStats(correlationId) { - try { - logger.info('Fetching system statistics', { correlationId }); + async getSystemStats(correlationId) { + try { + logger.info('Fetching system statistics', { correlationId }); - // Get player counts - const playerStats = await db('players') - .select([ - db.raw('COUNT(*) as total_players'), - db.raw('COUNT(CASE WHEN is_active = true THEN 1 END) as active_players'), - db.raw('COUNT(CASE WHEN is_verified = true THEN 1 END) as verified_players'), - db.raw('COUNT(CASE WHEN created_at >= NOW() - INTERVAL \'24 hours\' THEN 1 END) as new_players_24h'), - ]) - .first(); + // Get player counts + const playerStats = await db('players') + .select([ + db.raw('COUNT(*) as total_players'), + db.raw('COUNT(CASE WHEN is_active = true THEN 1 END) as active_players'), + db.raw('COUNT(CASE WHEN is_verified = true THEN 1 END) as verified_players'), + db.raw('COUNT(CASE WHEN created_at >= NOW() - INTERVAL \'24 hours\' THEN 1 END) as new_players_24h') + ]) + .first(); - // Get colony and fleet counts - const gameStats = await db.raw(` + // Get colony and fleet counts + const gameStats = await db.raw(` SELECT (SELECT COUNT(*) FROM colonies) as total_colonies, (SELECT COUNT(*) FROM fleets) as total_fleets, (SELECT COUNT(*) FROM research_queue) as active_research `); - // Get recent activity (last 24 hours) - const recentActivity = await db('players') - .select([ - db.raw('COUNT(CASE WHEN last_login_at >= NOW() - INTERVAL \'24 hours\' THEN 1 END) as active_24h'), - db.raw('COUNT(CASE WHEN last_login_at >= NOW() - INTERVAL \'7 days\' THEN 1 END) as active_7d'), - ]) - .first(); + // Get recent activity (last 24 hours) + const recentActivity = await db('players') + .select([ + db.raw('COUNT(CASE WHEN last_login_at >= NOW() - INTERVAL \'24 hours\' THEN 1 END) as active_24h'), + db.raw('COUNT(CASE WHEN last_login_at >= NOW() - INTERVAL \'7 days\' THEN 1 END) as active_7d') + ]) + .first(); - const stats = { - players: { - total: parseInt(playerStats.total_players), - active: parseInt(playerStats.active_players), - verified: parseInt(playerStats.verified_players), - newToday: parseInt(playerStats.new_players_24h), - }, - game: { - totalColonies: parseInt(gameStats.rows[0].total_colonies), - totalFleets: parseInt(gameStats.rows[0].total_fleets), - activeResearch: parseInt(gameStats.rows[0].active_research), - }, - activity: { - active24h: parseInt(recentActivity.active_24h), - active7d: parseInt(recentActivity.active_7d), - }, - timestamp: new Date().toISOString(), - }; + const stats = { + players: { + total: parseInt(playerStats.total_players), + active: parseInt(playerStats.active_players), + verified: parseInt(playerStats.verified_players), + newToday: parseInt(playerStats.new_players_24h) + }, + game: { + totalColonies: parseInt(gameStats.rows[0].total_colonies), + totalFleets: parseInt(gameStats.rows[0].total_fleets), + activeResearch: parseInt(gameStats.rows[0].active_research) + }, + activity: { + active24h: parseInt(recentActivity.active_24h), + active7d: parseInt(recentActivity.active_7d) + }, + timestamp: new Date().toISOString() + }; - logger.info('System statistics retrieved', { - correlationId, - totalPlayers: stats.players.total, - activePlayers: stats.players.active, - }); + logger.info('System statistics retrieved', { + correlationId, + totalPlayers: stats.players.total, + activePlayers: stats.players.active + }); - return stats; + return stats; - } catch (error) { - logger.error('Failed to fetch system statistics', { - correlationId, - error: error.message, - stack: error.stack, - }); + } catch (error) { + logger.error('Failed to fetch system statistics', { + correlationId, + error: error.message, + stack: error.stack + }); - throw new Error('Failed to retrieve system statistics'); + throw new Error('Failed to retrieve system statistics'); + } } - } - /** + /** * Find admin by email * @param {string} email - Admin email * @returns {Promise} Admin data or null */ - async findAdminByEmail(email) { - try { - return await db('admins') - .where('email', email.toLowerCase().trim()) - .first(); - } catch (error) { - logger.error('Failed to find admin by email', { error: error.message }); - return null; + async findAdminByEmail(email) { + try { + return await db('admins') + .where('email', email.toLowerCase().trim()) + .first(); + } catch (error) { + logger.error('Failed to find admin by email', { error: error.message }); + return null; + } } - } - /** + /** * Get admin permissions * @param {number} adminId - Admin ID * @returns {Promise} Array of permission strings */ - async getAdminPermissions(adminId) { - try { - const permissions = await db('admin_permissions as ap') - .join('permissions as p', 'ap.permission_id', 'p.id') - .select('p.name') - .where('ap.admin_id', adminId); + async getAdminPermissions(adminId) { + try { + const permissions = await db('admin_permissions as ap') + .join('permissions as p', 'ap.permission_id', 'p.id') + .select('p.name') + .where('ap.admin_id', adminId); - return permissions.map(p => p.name); - } catch (error) { - logger.error('Failed to fetch admin permissions', { - adminId, - error: error.message, - }); - return []; + return permissions.map(p => p.name); + } catch (error) { + logger.error('Failed to fetch admin permissions', { + adminId, + error: error.message + }); + return []; + } } - } - /** + /** * Check if admin has specific permission * @param {number} adminId - Admin ID * @param {string} permission - Permission to check * @returns {Promise} True if admin has permission */ - async hasPermission(adminId, permission) { - try { - const permissions = await this.getAdminPermissions(adminId); - return permissions.includes(permission) || permissions.includes('super_admin'); - } catch (error) { - logger.error('Failed to check admin permission', { - adminId, - permission, - error: error.message, - }); - return false; + async hasPermission(adminId, permission) { + try { + const permissions = await this.getAdminPermissions(adminId); + return permissions.includes(permission) || permissions.includes('super_admin'); + } catch (error) { + logger.error('Failed to check admin permission', { + adminId, + permission, + error: error.message + }); + return false; + } } - } } -module.exports = AdminService; +module.exports = AdminService; \ No newline at end of file diff --git a/src/services/user/PlayerService.js b/src/services/user/PlayerService.js index 29d2170..c17398c 100644 --- a/src/services/user/PlayerService.js +++ b/src/services/user/PlayerService.js @@ -7,20 +7,11 @@ const db = require('../../database/connection'); const { hashPassword, verifyPassword, validatePasswordStrength } = require('../../utils/password'); const { generatePlayerToken, generateRefreshToken } = require('../../utils/jwt'); const { validateEmail, validateUsername } = require('../../utils/validation'); -const { validatePasswordStrength: validateSecurePassword } = require('../../utils/security'); const logger = require('../../utils/logger'); const { ValidationError, ConflictError, NotFoundError, AuthenticationError } = require('../../middleware/error.middleware'); -const ResourceService = require('../resource/ResourceService'); -const EmailService = require('../auth/EmailService'); -const TokenService = require('../auth/TokenService'); class PlayerService { - constructor() { - this.resourceService = new ResourceService(); - this.emailService = new EmailService(); - this.tokenService = new TokenService(); - } - /** + /** * Register a new player * @param {Object} playerData - Player registration data * @param {string} playerData.email - Player email @@ -29,1007 +20,453 @@ class PlayerService { * @param {string} correlationId - Request correlation ID * @returns {Promise} Registered player data */ - async registerPlayer(playerData, correlationId) { - try { - const { email, username, password } = playerData; + async registerPlayer(playerData, correlationId) { + try { + const { email, username, password } = playerData; - logger.info('Player registration initiated', { - correlationId, - email, - username, - }); + logger.info('Player registration initiated', { + correlationId, + email, + username + }); - // Validate input data - await this.validateRegistrationData({ email, username, password }); + // Validate input data + await this.validateRegistrationData({ email, username, password }); - // Check if email already exists - const existingEmail = await this.findPlayerByEmail(email); - if (existingEmail) { - throw new ConflictError('Email address is already registered'); - } + // Check if email already exists + const existingEmail = await this.findPlayerByEmail(email); + if (existingEmail) { + throw new ConflictError('Email address is already registered'); + } - // Check if username already exists - const existingUsername = await this.findPlayerByUsername(username); - if (existingUsername) { - throw new ConflictError('Username is already taken'); - } + // Check if username already exists + const existingUsername = await this.findPlayerByUsername(username); + if (existingUsername) { + throw new ConflictError('Username is already taken'); + } - // Hash password - const hashedPassword = await hashPassword(password); + // Hash password + const hashedPassword = await hashPassword(password); - // Create player in database transaction - const player = await db.transaction(async (trx) => { - // Generate user group assignment (for game tick processing) - const userGroup = Math.floor(Math.random() * 10); - - const [newPlayer] = await trx('players') - .insert({ - email: email.toLowerCase().trim(), - username: username.trim(), - password_hash: hashedPassword, - email_verified: false, // Email verification required - user_group: userGroup, - is_active: true, - is_banned: false, - created_at: new Date(), - updated_at: new Date(), - }) - .returning(['id', 'email', 'username', 'email_verified', 'is_active', 'created_at']); + // Create player in database transaction + const player = await db.transaction(async (trx) => { + const [newPlayer] = await trx('players') + .insert({ + email: email.toLowerCase().trim(), + username: username.trim(), + password_hash: hashedPassword, + is_active: true, + is_verified: false, // Email verification required + created_at: new Date(), + updated_at: new Date() + }) + .returning(['id', 'email', 'username', 'is_active', 'is_verified', 'created_at']); - // Initialize player resources using ResourceService - await this.resourceService.initializePlayerResources(newPlayer.id, trx); + // Create initial player resources + await trx('player_resources').insert({ + player_id: newPlayer.id, + scrap: parseInt(process.env.STARTING_RESOURCES_SCRAP) || 1000, + energy: parseInt(process.env.STARTING_RESOURCES_ENERGY) || 500, + research_points: 0, + created_at: new Date(), + updated_at: new Date() + }); - // Create initial player stats - await trx('player_stats').insert({ - player_id: newPlayer.id, - colonies_count: 0, - fleets_count: 0, - total_battles: 0, - battles_won: 0, - created_at: new Date(), - updated_at: new Date(), - }); + // Create initial player stats + await trx('player_stats').insert({ + player_id: newPlayer.id, + colonies_count: 0, + fleets_count: 0, + total_battles: 0, + battles_won: 0, + created_at: new Date(), + updated_at: new Date() + }); - logger.info('Player registered successfully', { - correlationId, - playerId: newPlayer.id, - email: newPlayer.email, - username: newPlayer.username, - }); + logger.info('Player registered successfully', { + correlationId, + playerId: newPlayer.id, + email: newPlayer.email, + username: newPlayer.username + }); - return newPlayer; - }); + return newPlayer; + }); - // Generate and send email verification token - try { - const verificationToken = await this.tokenService.generateEmailVerificationToken( - player.id, - player.email - ); - - await this.emailService.sendEmailVerification( - player.email, - player.username, - verificationToken, - correlationId - ); - - logger.info('Verification email sent', { - correlationId, - playerId: player.id, - email: player.email, - }); - } catch (emailError) { - logger.error('Failed to send verification email', { - correlationId, - playerId: player.id, - error: emailError.message, - }); - // Don't fail registration if email fails - } + // Return player data without sensitive information + return { + id: player.id, + email: player.email, + username: player.username, + isActive: player.is_active, + isVerified: player.is_verified, + createdAt: player.created_at + }; - // Return player data without sensitive information - return { - id: player.id, - email: player.email, - username: player.username, - isActive: player.is_active, - isVerified: player.email_verified, - createdAt: player.created_at, - verificationEmailSent: true, - }; + } catch (error) { + logger.error('Player registration failed', { + correlationId, + email: playerData.email, + username: playerData.username, + error: error.message, + stack: error.stack + }); - } catch (error) { - logger.error('Player registration failed', { - correlationId, - email: playerData.email, - username: playerData.username, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError || error instanceof ConflictError) { - throw error; - } - throw new Error('Player registration failed'); + if (error instanceof ValidationError || error instanceof ConflictError) { + throw error; + } + throw new Error('Player registration failed'); + } } - } - /** + /** * Authenticate player login * @param {Object} loginData - Login credentials * @param {string} loginData.email - Player email * @param {string} loginData.password - Player password - * @param {string} loginData.ipAddress - Client IP address - * @param {string} loginData.userAgent - Client user agent * @param {string} correlationId - Request correlation ID * @returns {Promise} Authentication result with tokens */ - async authenticatePlayer(loginData, correlationId) { - try { - const { email, password, ipAddress, userAgent } = loginData; + async authenticatePlayer(loginData, correlationId) { + try { + const { email, password } = loginData; - logger.info('Player authentication initiated', { - correlationId, - email, - ipAddress, - }); + logger.info('Player authentication initiated', { + correlationId, + email + }); - // Check for account lockout - const lockoutStatus = await this.tokenService.isAccountLocked(email); - if (lockoutStatus.isLocked) { - logger.warn('Authentication blocked - account locked', { - correlationId, - email, - lockedUntil: lockoutStatus.expiresAt, - }); - throw new AuthenticationError(`Account temporarily locked. Try again after ${lockoutStatus.expiresAt.toLocaleString()}`); - } + // Find player by email + const player = await this.findPlayerByEmail(email); + if (!player) { + throw new AuthenticationError('Invalid email or password'); + } - // Find player by email - const player = await this.findPlayerByEmail(email); - if (!player) { - throw new AuthenticationError('Invalid email or password'); - } + // Check if player is active + if (!player.is_active) { + throw new AuthenticationError('Account has been deactivated'); + } - // Check if player is active - if (!player.is_active) { - throw new AuthenticationError('Account has been deactivated'); - } + // Verify password + const isPasswordValid = await verifyPassword(password, player.password_hash); + if (!isPasswordValid) { + logger.warn('Player authentication failed - invalid password', { + correlationId, + playerId: player.id, + email: player.email + }); + throw new AuthenticationError('Invalid email or password'); + } - // Verify password - const isPasswordValid = await verifyPassword(password, player.password_hash); - if (!isPasswordValid) { - logger.warn('Player authentication failed - invalid password', { - correlationId, - playerId: player.id, - email: player.email, - ipAddress, - }); - - // Track failed attempt - await this.tokenService.trackFailedAttempt(email); - - throw new AuthenticationError('Invalid email or password'); - } + // Generate tokens + const accessToken = generatePlayerToken({ + playerId: player.id, + email: player.email, + username: player.username + }); - // Clear any previous failed attempts on successful login - await this.tokenService.clearFailedAttempts(email); + const refreshToken = generateRefreshToken({ + userId: player.id, + type: 'player' + }); - // Generate tokens using TokenService - const tokens = await this.tokenService.generateAuthTokens({ - id: player.id, - email: player.email, - username: player.username, - userAgent, - ipAddress, - }); + // Update last login timestamp + await db('players') + .where('id', player.id) + .update({ + last_login_at: new Date(), + updated_at: new Date() + }); - // Update last login timestamp - await db('players') - .where('id', player.id) - .update({ - last_login: new Date(), - updated_at: new Date(), - }); + logger.info('Player authenticated successfully', { + correlationId, + playerId: player.id, + email: player.email, + username: player.username + }); - logger.info('Player authenticated successfully', { - correlationId, - playerId: player.id, - email: player.email, - username: player.username, - }); + return { + player: { + id: player.id, + email: player.email, + username: player.username, + isActive: player.is_active, + isVerified: player.is_verified + }, + tokens: { + accessToken, + refreshToken + } + }; - return { - player: { - id: player.id, - email: player.email, - username: player.username, - isActive: player.is_active, - isVerified: player.email_verified, - isBanned: player.is_banned, - }, - tokens: { - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken, - }, - }; + } catch (error) { + logger.error('Player authentication failed', { + correlationId, + email: loginData.email, + error: error.message + }); - } catch (error) { - logger.error('Player authentication failed', { - correlationId, - email: loginData.email, - error: error.message, - }); - - if (error instanceof AuthenticationError) { - throw error; - } - throw new AuthenticationError('Authentication failed'); + if (error instanceof AuthenticationError) { + throw error; + } + throw new AuthenticationError('Authentication failed'); + } } - } - /** + /** * Get player profile by ID * @param {number} playerId - Player ID * @param {string} correlationId - Request correlation ID * @returns {Promise} Player profile data */ - async getPlayerProfile(playerId, correlationId) { - try { - logger.info('Fetching player profile', { - correlationId, - playerId, - }); + async getPlayerProfile(playerId, correlationId) { + try { + logger.info('Fetching player profile', { + correlationId, + playerId + }); - const player = await db('players') - .select([ - 'id', - 'email', - 'username', - 'is_active', - 'email_verified', - 'is_banned', - 'created_at', - 'last_login', - ]) - .where('id', playerId) - .first(); + const player = await db('players') + .select([ + 'id', + 'email', + 'username', + 'is_active', + 'is_verified', + 'created_at', + 'last_login_at' + ]) + .where('id', playerId) + .first(); - if (!player) { - throw new NotFoundError('Player not found'); - } + if (!player) { + throw new NotFoundError('Player not found'); + } - // Get player resources - const resources = await this.resourceService.getPlayerResourceSummary(playerId, correlationId); + // Get player resources + const resources = await db('player_resources') + .select(['scrap', 'energy', 'research_points']) + .where('player_id', playerId) + .first(); - // Get player stats - const stats = await db('player_stats') - .select([ - 'colonies_count', - 'fleets_count', - 'total_battles', - 'battles_won', - ]) - .where('player_id', playerId) - .first(); + // Get player stats + const stats = await db('player_stats') + .select([ + 'colonies_count', + 'fleets_count', + 'total_battles', + 'battles_won' + ]) + .where('player_id', playerId) + .first(); - const profile = { - id: player.id, - email: player.email, - username: player.username, - isActive: player.is_active, - isVerified: player.email_verified, - isBanned: player.is_banned, - createdAt: player.created_at, - lastLoginAt: player.last_login, - resources: resources || {}, - stats: stats || { - coloniesCount: 0, - fleetsCount: 0, - totalBattles: 0, - battlesWon: 0, - }, - }; + const profile = { + id: player.id, + email: player.email, + username: player.username, + isActive: player.is_active, + isVerified: player.is_verified, + createdAt: player.created_at, + lastLoginAt: player.last_login_at, + resources: resources || { + scrap: 0, + energy: 0, + researchPoints: 0 + }, + stats: stats || { + coloniesCount: 0, + fleetsCount: 0, + totalBattles: 0, + battlesWon: 0 + } + }; - logger.info('Player profile retrieved successfully', { - correlationId, - playerId, - username: player.username, - }); + logger.info('Player profile retrieved successfully', { + correlationId, + playerId, + username: player.username + }); - return profile; + return profile; - } catch (error) { - logger.error('Failed to fetch player profile', { - correlationId, - playerId, - error: error.message, - stack: error.stack, - }); + } catch (error) { + logger.error('Failed to fetch player profile', { + correlationId, + playerId, + error: error.message, + stack: error.stack + }); - if (error instanceof NotFoundError) { - throw error; - } - throw new Error('Failed to retrieve player profile'); + if (error instanceof NotFoundError) { + throw error; + } + throw new Error('Failed to retrieve player profile'); + } } - } - /** + /** * Update player profile * @param {number} playerId - Player ID * @param {Object} updateData - Data to update * @param {string} correlationId - Request correlation ID * @returns {Promise} Updated player profile */ - async updatePlayerProfile(playerId, updateData, correlationId) { - try { - logger.info('Updating player profile', { - correlationId, - playerId, - updateFields: Object.keys(updateData), - }); + async updatePlayerProfile(playerId, updateData, correlationId) { + try { + logger.info('Updating player profile', { + correlationId, + playerId, + updateFields: Object.keys(updateData) + }); - // Validate player exists - const existingPlayer = await this.findPlayerById(playerId); - if (!existingPlayer) { - throw new NotFoundError('Player not found'); - } + // Validate player exists + const existingPlayer = await this.findPlayerById(playerId); + if (!existingPlayer) { + throw new NotFoundError('Player not found'); + } - // Validate update data - const allowedFields = ['username']; - const sanitizedData = {}; + // Validate update data + const allowedFields = ['username']; + const sanitizedData = {}; - for (const [key, value] of Object.entries(updateData)) { - if (allowedFields.includes(key)) { - sanitizedData[key] = value; + for (const [key, value] of Object.entries(updateData)) { + if (allowedFields.includes(key)) { + sanitizedData[key] = value; + } + } + + // Validate username if being updated + if (sanitizedData.username) { + const usernameValidation = validateUsername(sanitizedData.username); + if (!usernameValidation.isValid) { + throw new ValidationError(usernameValidation.error); + } + + // Check if username is already taken by another player + const existingUsername = await db('players') + .where('username', sanitizedData.username) + .where('id', '!=', playerId) + .first(); + + if (existingUsername) { + throw new ConflictError('Username is already taken'); + } + } + + if (Object.keys(sanitizedData).length === 0) { + throw new ValidationError('No valid fields to update'); + } + + // Update player + sanitizedData.updated_at = new Date(); + await db('players') + .where('id', playerId) + .update(sanitizedData); + + // Return updated profile + const updatedProfile = await this.getPlayerProfile(playerId, correlationId); + + logger.info('Player profile updated successfully', { + correlationId, + playerId, + updatedFields: Object.keys(sanitizedData) + }); + + return updatedProfile; + + } catch (error) { + logger.error('Failed to update player profile', { + correlationId, + playerId, + error: error.message, + stack: error.stack + }); + + if (error instanceof ValidationError || error instanceof ConflictError || error instanceof NotFoundError) { + throw error; + } + throw new Error('Failed to update player profile'); } - } - - // Validate username if being updated - if (sanitizedData.username) { - const usernameValidation = validateUsername(sanitizedData.username); - if (!usernameValidation.isValid) { - throw new ValidationError(usernameValidation.error); - } - - // Check if username is already taken by another player - const existingUsername = await db('players') - .where('username', sanitizedData.username) - .where('id', '!=', playerId) - .first(); - - if (existingUsername) { - throw new ConflictError('Username is already taken'); - } - } - - if (Object.keys(sanitizedData).length === 0) { - throw new ValidationError('No valid fields to update'); - } - - // Update player - sanitizedData.updated_at = new Date(); - await db('players') - .where('id', playerId) - .update(sanitizedData); - - // Return updated profile - const updatedProfile = await this.getPlayerProfile(playerId, correlationId); - - logger.info('Player profile updated successfully', { - correlationId, - playerId, - updatedFields: Object.keys(sanitizedData), - }); - - return updatedProfile; - - } catch (error) { - logger.error('Failed to update player profile', { - correlationId, - playerId, - error: error.message, - stack: error.stack, - }); - - if (error instanceof ValidationError || error instanceof ConflictError || error instanceof NotFoundError) { - throw error; - } - throw new Error('Failed to update player profile'); } - } - /** + /** * Find player by email * @param {string} email - Player email * @returns {Promise} Player data or null */ - async findPlayerByEmail(email) { - try { - return await db('players') - .where('email', email.toLowerCase().trim()) - .first(); - } catch (error) { - logger.error('Failed to find player by email', { error: error.message }); - return null; + async findPlayerByEmail(email) { + try { + return await db('players') + .where('email', email.toLowerCase().trim()) + .first(); + } catch (error) { + logger.error('Failed to find player by email', { error: error.message }); + return null; + } } - } - /** + /** * Find player by username * @param {string} username - Player username * @returns {Promise} Player data or null */ - async findPlayerByUsername(username) { - try { - return await db('players') - .where('username', username.trim()) - .first(); - } catch (error) { - logger.error('Failed to find player by username', { error: error.message }); - return null; + async findPlayerByUsername(username) { + try { + return await db('players') + .where('username', username.trim()) + .first(); + } catch (error) { + logger.error('Failed to find player by username', { error: error.message }); + return null; + } } - } - /** + /** * Find player by ID * @param {number} playerId - Player ID * @returns {Promise} Player data or null */ - async findPlayerById(playerId) { - try { - return await db('players') - .where('id', playerId) - .first(); - } catch (error) { - logger.error('Failed to find player by ID', { error: error.message }); - return null; + async findPlayerById(playerId) { + try { + return await db('players') + .where('id', playerId) + .first(); + } catch (error) { + logger.error('Failed to find player by ID', { error: error.message }); + return null; + } } - } - /** + /** * Validate registration data * @param {Object} data - Registration data to validate * @returns {Promise} * @throws {ValidationError} If validation fails */ - async validateRegistrationData(data) { - const { email, username, password } = data; + async validateRegistrationData(data) { + const { email, username, password } = data; - // Validate email - const emailValidation = validateEmail(email); - if (!emailValidation.isValid) { - throw new ValidationError(emailValidation.error); + // Validate email + const emailValidation = validateEmail(email); + if (!emailValidation.isValid) { + throw new ValidationError(emailValidation.error); + } + + // Validate username + const usernameValidation = validateUsername(username); + if (!usernameValidation.isValid) { + throw new ValidationError(usernameValidation.error); + } + + // Validate password strength + const passwordValidation = validatePasswordStrength(password); + if (!passwordValidation.isValid) { + throw new ValidationError('Password does not meet requirements', { + requirements: passwordValidation.requirements, + errors: passwordValidation.errors + }); + } } - - // Validate username - const usernameValidation = validateUsername(username); - if (!usernameValidation.isValid) { - throw new ValidationError(usernameValidation.error); - } - - // Validate password strength (using relaxed validation) - const passwordValidation = validateSecurePassword(password); - if (!passwordValidation.isValid) { - throw new ValidationError('Password does not meet requirements', { - requirements: passwordValidation.requirements, - errors: passwordValidation.errors, - }); - } - } - - /** - * Verify player email address - * @param {string} token - Email verification token - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Verification result - */ - async verifyEmail(token, correlationId) { - try { - logger.info('Email verification initiated', { - correlationId, - tokenPrefix: token.substring(0, 8) + '...', - }); - - // Validate token - const tokenData = await this.tokenService.validateSecurityToken(token, 'email_verification'); - - // Find player - const player = await this.findPlayerById(tokenData.playerId); - if (!player) { - throw new NotFoundError('Player not found'); - } - - // Check if already verified - if (player.email_verified) { - logger.info('Email already verified', { - correlationId, - playerId: player.id, - email: player.email, - }); - - return { - success: true, - message: 'Email is already verified', - player: { - id: player.id, - email: player.email, - username: player.username, - isVerified: true, - }, - }; - } - - // Verify email addresses match - if (player.email !== tokenData.email) { - logger.warn('Email verification token email mismatch', { - correlationId, - playerId: player.id, - playerEmail: player.email, - tokenEmail: tokenData.email, - }); - throw new ValidationError('Invalid verification token'); - } - - // Update player as verified - await db('players') - .where('id', player.id) - .update({ - email_verified: true, - updated_at: new Date(), - }); - - logger.info('Email verified successfully', { - correlationId, - playerId: player.id, - email: player.email, - }); - - return { - success: true, - message: 'Email verified successfully', - player: { - id: player.id, - email: player.email, - username: player.username, - isVerified: true, - }, - }; - - } catch (error) { - logger.error('Email verification failed', { - correlationId, - tokenPrefix: token.substring(0, 8) + '...', - error: error.message, - }); - - if (error instanceof ValidationError || error instanceof NotFoundError) { - throw error; - } - throw new Error('Email verification failed'); - } - } - - /** - * Resend email verification - * @param {string} email - Player email - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Resend result - */ - async resendEmailVerification(email, correlationId) { - try { - logger.info('Resending email verification', { - correlationId, - email, - }); - - // Find player - const player = await this.findPlayerByEmail(email); - if (!player) { - // Don't reveal if email exists or not - logger.info('Email verification resend requested for non-existent email', { - correlationId, - email, - }); - return { - success: true, - message: 'If the email exists in our system, a verification email has been sent', - }; - } - - // Check if already verified - if (player.email_verified) { - return { - success: true, - message: 'Email is already verified', - }; - } - - // Generate and send new verification token - const verificationToken = await this.tokenService.generateEmailVerificationToken( - player.id, - player.email - ); - - await this.emailService.sendEmailVerification( - player.email, - player.username, - verificationToken, - correlationId - ); - - logger.info('Verification email resent', { - correlationId, - playerId: player.id, - email: player.email, - }); - - return { - success: true, - message: 'Verification email sent', - }; - - } catch (error) { - logger.error('Failed to resend email verification', { - correlationId, - email, - error: error.message, - }); - - // Don't reveal internal errors to users - return { - success: true, - message: 'If the email exists in our system, a verification email has been sent', - }; - } - } - - /** - * Request password reset - * @param {string} email - Player email - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Reset request result - */ - async requestPasswordReset(email, correlationId) { - try { - logger.info('Password reset requested', { - correlationId, - email, - }); - - // Find player - const player = await this.findPlayerByEmail(email); - if (!player) { - // Don't reveal if email exists or not - logger.info('Password reset requested for non-existent email', { - correlationId, - email, - }); - return { - success: true, - message: 'If the email exists in our system, a password reset email has been sent', - }; - } - - // Check if account is active - if (!player.is_active || player.is_banned) { - logger.warn('Password reset requested for inactive/banned account', { - correlationId, - playerId: player.id, - email, - isActive: player.is_active, - isBanned: player.is_banned, - }); - return { - success: true, - message: 'If the email exists in our system, a password reset email has been sent', - }; - } - - // Generate password reset token - const resetToken = await this.tokenService.generatePasswordResetToken( - player.id, - player.email - ); - - // Send password reset email - await this.emailService.sendPasswordReset( - player.email, - player.username, - resetToken, - correlationId - ); - - logger.info('Password reset email sent', { - correlationId, - playerId: player.id, - email: player.email, - }); - - return { - success: true, - message: 'If the email exists in our system, a password reset email has been sent', - }; - - } catch (error) { - logger.error('Failed to send password reset email', { - correlationId, - email, - error: error.message, - }); - - // Don't reveal internal errors to users - return { - success: true, - message: 'If the email exists in our system, a password reset email has been sent', - }; - } - } - - /** - * Reset password using token - * @param {string} token - Password reset token - * @param {string} newPassword - New password - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Reset result - */ - async resetPassword(token, newPassword, correlationId) { - try { - logger.info('Password reset initiated', { - correlationId, - tokenPrefix: token.substring(0, 8) + '...', - }); - - // Validate new password - const passwordValidation = validateSecurePassword(newPassword); - if (!passwordValidation.isValid) { - throw new ValidationError('New password does not meet requirements', { - requirements: passwordValidation.requirements, - errors: passwordValidation.errors, - }); - } - - // Validate token - const tokenData = await this.tokenService.validateSecurityToken(token, 'password_reset'); - - // Find player - const player = await this.findPlayerById(tokenData.playerId); - if (!player) { - throw new NotFoundError('Player not found'); - } - - // Verify email addresses match - if (player.email !== tokenData.email) { - logger.warn('Password reset token email mismatch', { - correlationId, - playerId: player.id, - playerEmail: player.email, - tokenEmail: tokenData.email, - }); - throw new ValidationError('Invalid reset token'); - } - - // Hash new password - const hashedPassword = await hashPassword(newPassword); - - // Update password and clear reset fields - await db('players') - .where('id', player.id) - .update({ - password_hash: hashedPassword, - reset_password_token: null, - reset_password_expires: null, - updated_at: new Date(), - }); - - // Revoke all existing refresh tokens for security - await this.tokenService.revokeAllUserTokens(player.id); - - logger.info('Password reset successfully', { - correlationId, - playerId: player.id, - email: player.email, - }); - - // Send security alert email - try { - await this.emailService.sendSecurityAlert( - player.email, - player.username, - 'Password Reset', - { - action: 'Password successfully reset', - timestamp: new Date().toISOString(), - }, - correlationId - ); - } catch (emailError) { - logger.warn('Failed to send password reset security alert', { - correlationId, - playerId: player.id, - error: emailError.message, - }); - } - - return { - success: true, - message: 'Password reset successfully', - }; - - } catch (error) { - logger.error('Password reset failed', { - correlationId, - tokenPrefix: token.substring(0, 8) + '...', - error: error.message, - }); - - if (error instanceof ValidationError || error instanceof NotFoundError) { - throw error; - } - throw new Error('Password reset failed'); - } - } - - /** - * Change password (authenticated user) - * @param {number} playerId - Player ID - * @param {string} currentPassword - Current password - * @param {string} newPassword - New password - * @param {string} correlationId - Request correlation ID - * @returns {Promise} Change result - */ - async changePassword(playerId, currentPassword, newPassword, correlationId) { - try { - logger.info('Password change initiated', { - correlationId, - playerId, - }); - - // Find player - const player = await this.findPlayerById(playerId); - if (!player) { - throw new NotFoundError('Player not found'); - } - - // Verify current password - const isCurrentPasswordValid = await verifyPassword(currentPassword, player.password_hash); - if (!isCurrentPasswordValid) { - logger.warn('Password change failed - invalid current password', { - correlationId, - playerId, - }); - throw new AuthenticationError('Current password is incorrect'); - } - - // Validate new password - const passwordValidation = validateSecurePassword(newPassword); - if (!passwordValidation.isValid) { - throw new ValidationError('New password does not meet requirements', { - requirements: passwordValidation.requirements, - errors: passwordValidation.errors, - }); - } - - // Check if new password is different from current - const isSamePassword = await verifyPassword(newPassword, player.password_hash); - if (isSamePassword) { - throw new ValidationError('New password must be different from current password'); - } - - // Hash new password - const hashedPassword = await hashPassword(newPassword); - - // Update password - await db('players') - .where('id', playerId) - .update({ - password_hash: hashedPassword, - updated_at: new Date(), - }); - - // Revoke all existing refresh tokens for security - await this.tokenService.revokeAllUserTokens(playerId); - - logger.info('Password changed successfully', { - correlationId, - playerId, - }); - - // Send security alert email - try { - await this.emailService.sendSecurityAlert( - player.email, - player.username, - 'Password Changed', - { - action: 'Password successfully changed', - timestamp: new Date().toISOString(), - }, - correlationId - ); - } catch (emailError) { - logger.warn('Failed to send password change security alert', { - correlationId, - playerId, - error: emailError.message, - }); - } - - return { - success: true, - message: 'Password changed successfully', - }; - - } catch (error) { - logger.error('Password change failed', { - correlationId, - playerId, - error: error.message, - }); - - if (error instanceof ValidationError || error instanceof NotFoundError || error instanceof AuthenticationError) { - throw error; - } - throw new Error('Password change failed'); - } - } - - /** - * Refresh access token - * @param {string} refreshToken - Refresh token - * @param {string} correlationId - Request correlation ID - * @returns {Promise} New access token - */ - async refreshAccessToken(refreshToken, correlationId) { - try { - return await this.tokenService.refreshAccessToken(refreshToken, correlationId); - } catch (error) { - logger.error('Token refresh failed in PlayerService', { - correlationId, - error: error.message, - }); - throw error; - } - } - - /** - * Logout user by blacklisting tokens - * @param {string} accessToken - Access token to blacklist - * @param {string} refreshTokenId - Refresh token ID to revoke - * @param {string} correlationId - Request correlation ID - * @returns {Promise} - */ - async logoutPlayer(accessToken, refreshTokenId, correlationId) { - try { - logger.info('Player logout initiated', { - correlationId, - refreshTokenId, - }); - - // Blacklist access token - if (accessToken) { - await this.tokenService.blacklistToken(accessToken, 'logout'); - } - - // Revoke refresh token - if (refreshTokenId) { - await this.tokenService.revokeRefreshToken(refreshTokenId); - } - - logger.info('Player logout completed', { - correlationId, - refreshTokenId, - }); - - } catch (error) { - logger.error('Player logout failed', { - correlationId, - error: error.message, - }); - throw error; - } - } } -module.exports = PlayerService; +module.exports = PlayerService; \ No newline at end of file diff --git a/src/services/websocket/GameEventService.js b/src/services/websocket/GameEventService.js deleted file mode 100644 index 76b6e11..0000000 --- a/src/services/websocket/GameEventService.js +++ /dev/null @@ -1,1584 +0,0 @@ -/** - * Game Event Service - * Handles WebSocket event broadcasting for real-time game updates - */ - -const logger = require('../../utils/logger'); - -class GameEventService { - constructor(io) { - this.io = io; - } - - /** - * Emit colony creation event - * @param {number} playerId - Player ID - * @param {Object} colony - Colony data - * @param {string} correlationId - Request correlation ID - */ - emitColonyCreated(playerId, colony, correlationId) { - try { - const eventData = { - type: 'colony_created', - data: { - colony: { - id: colony.id, - name: colony.name, - coordinates: colony.coordinates, - planetType: colony.planet_type_name, - foundedAt: colony.founded_at, - }, - }, - timestamp: new Date().toISOString(), - correlationId, - }; - - // Send to the player who created the colony - this.io.to(`player:${playerId}`).emit('game_event', eventData); - - // Send to anyone watching this sector - if (colony.sector_id) { - this.io.to(`sector:${colony.sector_id}`).emit('game_event', eventData); - } - - logger.info('Colony creation event emitted', { - correlationId, - playerId, - colonyId: colony.id, - colonyName: colony.name, - }); - - } catch (error) { - logger.error('Failed to emit colony creation event', { - correlationId, - playerId, - colonyId: colony.id, - error: error.message, - }); - } - } - - /** - * Emit building construction event - * @param {number} playerId - Player ID - * @param {number} colonyId - Colony ID - * @param {Object} building - Building data - * @param {string} correlationId - Request correlation ID - */ - emitBuildingConstructed(playerId, colonyId, building, correlationId) { - try { - const eventData = { - type: 'building_constructed', - data: { - colonyId, - building: { - id: building.id, - buildingTypeId: building.building_type_id, - level: building.level, - constructedAt: building.created_at, - }, - }, - timestamp: new Date().toISOString(), - correlationId, - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('game_event', eventData); - - // Send to anyone watching this colony - this.io.to(`colony:${colonyId}`).emit('game_event', eventData); - - logger.info('Building construction event emitted', { - correlationId, - playerId, - colonyId, - buildingId: building.id, - }); - - } catch (error) { - logger.error('Failed to emit building construction event', { - correlationId, - playerId, - colonyId, - buildingId: building.id, - error: error.message, - }); - } - } - - /** - * Emit resource update event - * @param {number} playerId - Player ID - * @param {Object} resourceChanges - Resource changes - * @param {string} reason - Reason for resource change - * @param {string} correlationId - Request correlation ID - */ - emitResourcesUpdated(playerId, resourceChanges, reason, correlationId) { - try { - const eventData = { - type: 'resources_updated', - data: { - reason, - changes: resourceChanges, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('game_event', eventData); - - logger.debug('Resource update event emitted', { - correlationId, - playerId, - reason, - resourceChanges, - }); - - } catch (error) { - logger.error('Failed to emit resource update event', { - correlationId, - playerId, - reason, - error: error.message, - }); - } - } - - /** - * Emit resource production tick event - * @param {number} playerId - Player ID - * @param {number} colonyId - Colony ID - * @param {Object} productionData - Production data - * @param {string} correlationId - Request correlation ID - */ - emitResourceProduction(playerId, colonyId, productionData, correlationId) { - try { - const eventData = { - type: 'resource_production', - data: { - colonyId, - production: productionData, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('game_event', eventData); - - // Send to anyone watching this colony - this.io.to(`colony:${colonyId}`).emit('game_event', eventData); - - logger.debug('Resource production event emitted', { - correlationId, - playerId, - colonyId, - productionData, - }); - - } catch (error) { - logger.error('Failed to emit resource production event', { - correlationId, - playerId, - colonyId, - error: error.message, - }); - } - } - - /** - * Emit colony status update event - * @param {number} playerId - Player ID - * @param {number} colonyId - Colony ID - * @param {Object} statusUpdate - Status update data - * @param {string} correlationId - Request correlation ID - */ - emitColonyStatusUpdate(playerId, colonyId, statusUpdate, correlationId) { - try { - const eventData = { - type: 'colony_status_update', - data: { - colonyId, - update: statusUpdate, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('game_event', eventData); - - // Send to anyone watching this colony - this.io.to(`colony:${colonyId}`).emit('game_event', eventData); - - logger.debug('Colony status update event emitted', { - correlationId, - playerId, - colonyId, - update: statusUpdate, - }); - - } catch (error) { - logger.error('Failed to emit colony status update event', { - correlationId, - playerId, - colonyId, - error: error.message, - }); - } - } - - /** - * Emit error event to player - * @param {number} playerId - Player ID - * @param {string} errorType - Type of error - * @param {string} message - Error message - * @param {Object} details - Additional error details - * @param {string} correlationId - Request correlation ID - */ - emitErrorEvent(playerId, errorType, message, details = {}, correlationId) { - try { - const eventData = { - type: 'error', - data: { - errorType, - message, - details, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('game_event', eventData); - - logger.warn('Error event emitted to player', { - correlationId, - playerId, - errorType, - message, - }); - - } catch (error) { - logger.error('Failed to emit error event', { - correlationId, - playerId, - errorType, - error: error.message, - }); - } - } - - /** - * Emit notification event to player - * @param {number} playerId - Player ID - * @param {Object} notification - Notification data - * @param {string} correlationId - Request correlation ID - */ - emitNotification(playerId, notification, correlationId) { - try { - const eventData = { - type: 'notification', - data: { - notification: { - ...notification, - timestamp: new Date().toISOString(), - }, - }, - correlationId, - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('game_event', eventData); - - logger.debug('Notification event emitted', { - correlationId, - playerId, - notificationType: notification.type, - }); - - } catch (error) { - logger.error('Failed to emit notification event', { - correlationId, - playerId, - error: error.message, - }); - } - } - - /** - * Emit player status change event - * @param {number} playerId - Player ID - * @param {string} status - New status - * @param {Array} relevantPlayers - Players who should be notified - * @param {string} correlationId - Request correlation ID - */ - emitPlayerStatusChange(playerId, status, relevantPlayers = [], correlationId) { - try { - const eventData = { - type: 'player_status_change', - data: { - playerId, - status, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to relevant players (allies, faction members, etc.) - relevantPlayers.forEach(targetPlayerId => { - this.io.to(`player:${targetPlayerId}`).emit('game_event', eventData); - }); - - logger.debug('Player status change event emitted', { - correlationId, - playerId, - status, - notifiedPlayers: relevantPlayers.length, - }); - - } catch (error) { - logger.error('Failed to emit player status change event', { - correlationId, - playerId, - status, - error: error.message, - }); - } - } - - /** - * Emit system-wide announcement - * @param {string} message - Announcement message - * @param {string} type - Announcement type (info, warning, emergency) - * @param {Object} metadata - Additional metadata - * @param {string} correlationId - Request correlation ID - */ - emitSystemAnnouncement(message, type = 'info', metadata = {}, correlationId) { - try { - const eventData = { - type: 'system_announcement', - data: { - message, - announcementType: type, - metadata, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Broadcast to all connected players - this.io.emit('game_event', eventData); - - logger.info('System announcement emitted', { - correlationId, - announcementType: type, - message, - }); - - } catch (error) { - logger.error('Failed to emit system announcement', { - correlationId, - message, - error: error.message, - }); - } - } - - /** - * Get connected player count - * @returns {number} Number of connected players - */ - getConnectedPlayerCount() { - try { - const sockets = this.io.sockets.sockets; - let authenticatedCount = 0; - - sockets.forEach(socket => { - if (socket.authenticated && socket.playerId) { - authenticatedCount++; - } - }); - - return authenticatedCount; - - } catch (error) { - logger.error('Failed to get connected player count', { - error: error.message, - }); - return 0; - } - } - - /** - * Get players in a specific room - * @param {string} roomName - Room name - * @returns {Array} Array of player IDs in the room - */ - getPlayersInRoom(roomName) { - try { - const room = this.io.sockets.adapter.rooms.get(roomName); - if (!room) return []; - - const playerIds = []; - room.forEach(socketId => { - const socket = this.io.sockets.sockets.get(socketId); - if (socket && socket.authenticated && socket.playerId) { - playerIds.push(socket.playerId); - } - }); - - return playerIds; - - } catch (error) { - logger.error('Failed to get players in room', { - roomName, - error: error.message, - }); - return []; - } - } - - // === COMBAT-SPECIFIC EVENTS === - - /** - * Emit combat initiation event - * @param {Object} battle - Battle data - * @param {string} correlationId - Request correlation ID - */ - async emitCombatInitiated(battle, correlationId) { - try { - const participants = JSON.parse(battle.participants); - const eventData = { - type: 'combat_initiated', - data: { - battleId: battle.id, - battleType: battle.battle_type, - location: battle.location, - status: battle.status, - estimatedDuration: battle.estimated_duration, - participants: { - attacker_fleet_id: participants.attacker_fleet_id, - defender_fleet_id: participants.defender_fleet_id, - defender_colony_id: participants.defender_colony_id, - }, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to attacking player - if (participants.attacker_player_id) { - this.io.to(`player:${participants.attacker_player_id}`).emit('combat_event', eventData); - } - - // Send to defending player - const defendingPlayerId = await this.getDefendingPlayerId(participants); - if (defendingPlayerId && defendingPlayerId !== participants.attacker_player_id) { - this.io.to(`player:${defendingPlayerId}`).emit('combat_event', eventData); - } - - // Send to location spectators - this.io.to(`location:${battle.location}`).emit('combat_event', eventData); - - // Send to battle room for spectators - this.io.to(`battle:${battle.id}`).emit('combat_event', eventData); - - logger.info('Combat initiation event emitted', { - correlationId, - battleId: battle.id, - battleType: battle.battle_type, - location: battle.location, - }); - - } catch (error) { - logger.error('Failed to emit combat initiation event', { - correlationId, - battleId: battle.id, - error: error.message, - stack: error.stack, - }); - } - } - - /** - * Emit combat status update event - * @param {number} battleId - Battle ID - * @param {string} status - New battle status - * @param {Object} updateData - Additional update data - * @param {string} correlationId - Request correlation ID - */ - emitCombatStatusUpdate(battleId, status, updateData = {}, correlationId) { - try { - const eventData = { - type: 'combat_status_update', - data: { - battleId, - status, - updateData, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to battle room (participants and spectators) - this.io.to(`battle:${battleId}`).emit('combat_event', eventData); - - logger.debug('Combat status update event emitted', { - correlationId, - battleId, - status, - }); - - } catch (error) { - logger.error('Failed to emit combat status update event', { - correlationId, - battleId, - status, - error: error.message, - }); - } - } - - /** - * Emit combat round update event (for turn-based combat) - * @param {number} battleId - Battle ID - * @param {number} round - Round number - * @param {Object} roundData - Round data - * @param {string} correlationId - Request correlation ID - */ - emitCombatRoundUpdate(battleId, round, roundData, correlationId) { - try { - const eventData = { - type: 'combat_round_update', - data: { - battleId, - round, - roundData, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to battle room - this.io.to(`battle:${battleId}`).emit('combat_event', eventData); - - logger.debug('Combat round update event emitted', { - correlationId, - battleId, - round, - }); - - } catch (error) { - logger.error('Failed to emit combat round update event', { - correlationId, - battleId, - round, - error: error.message, - }); - } - } - - /** - * Emit combat completed event - * @param {Object} result - Combat result - * @param {string} correlationId - Request correlation ID - */ - async emitCombatCompleted(result, correlationId) { - try { - const eventData = { - type: 'combat_completed', - data: { - battleId: result.battleId, - encounterId: result.encounterId, - outcome: result.outcome, - casualties: result.casualties, - experience: result.experience, - loot: result.loot, - duration: result.duration, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to battle room - this.io.to(`battle:${result.battleId}`).emit('combat_event', eventData); - - // Send detailed results to participants only - const participantData = { - ...eventData, - data: { - ...eventData.data, - detailed_casualties: result.casualties, - detailed_loot: result.loot, - }, - }; - - const participants = await this.getBattleParticipants(result.battleId); - participants.forEach(playerId => { - this.io.to(`player:${playerId}`).emit('combat_event', participantData); - }); - - logger.info('Combat completed event emitted', { - correlationId, - battleId: result.battleId, - outcome: result.outcome, - duration: result.duration, - }); - - } catch (error) { - logger.error('Failed to emit combat completed event', { - correlationId, - battleId: result.battleId, - error: error.message, - stack: error.stack, - }); - } - } - - /** - * Emit fleet movement event - * @param {number} playerId - Player ID - * @param {Object} fleet - Fleet data - * @param {string} fromLocation - Origin location - * @param {string} toLocation - Destination location - * @param {string} correlationId - Request correlation ID - */ - emitFleetMovement(playerId, fleet, fromLocation, toLocation, correlationId) { - try { - const eventData = { - type: 'fleet_movement', - data: { - fleetId: fleet.id, - fleetName: fleet.name, - playerId, - fromLocation, - toLocation, - arrivalTime: fleet.arrival_time, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to fleet owner - this.io.to(`player:${playerId}`).emit('game_event', eventData); - - // Send to origin location watchers - this.io.to(`location:${fromLocation}`).emit('game_event', eventData); - - // Send to destination location watchers - if (fromLocation !== toLocation) { - this.io.to(`location:${toLocation}`).emit('game_event', eventData); - } - - logger.debug('Fleet movement event emitted', { - correlationId, - fleetId: fleet.id, - fromLocation, - toLocation, - }); - - } catch (error) { - logger.error('Failed to emit fleet movement event', { - correlationId, - fleetId: fleet.id, - error: error.message, - }); - } - } - - /** - * Emit fleet status change event - * @param {number} playerId - Player ID - * @param {Object} fleet - Fleet data - * @param {string} oldStatus - Previous status - * @param {string} newStatus - New status - * @param {string} correlationId - Request correlation ID - */ - emitFleetStatusChange(playerId, fleet, oldStatus, newStatus, correlationId) { - try { - const eventData = { - type: 'fleet_status_change', - data: { - fleetId: fleet.id, - fleetName: fleet.name, - playerId, - oldStatus, - newStatus, - location: fleet.current_location, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to fleet owner - this.io.to(`player:${playerId}`).emit('game_event', eventData); - - // Send to location watchers - this.io.to(`location:${fleet.current_location}`).emit('game_event', eventData); - - logger.debug('Fleet status change event emitted', { - correlationId, - fleetId: fleet.id, - oldStatus, - newStatus, - }); - - } catch (error) { - logger.error('Failed to emit fleet status change event', { - correlationId, - fleetId: fleet.id, - error: error.message, - }); - } - } - - /** - * Emit colony under siege event - * @param {number} playerId - Colony owner player ID - * @param {Object} colony - Colony data - * @param {number} attackerFleetId - Attacking fleet ID - * @param {string} correlationId - Request correlation ID - */ - emitColonyUnderSiege(playerId, colony, attackerFleetId, correlationId) { - try { - const eventData = { - type: 'colony_under_siege', - data: { - colonyId: colony.id, - colonyName: colony.name, - coordinates: colony.coordinates, - attackerFleetId, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to colony owner - this.io.to(`player:${playerId}`).emit('combat_event', eventData); - - // Send to location watchers - this.io.to(`location:${colony.coordinates}`).emit('combat_event', eventData); - - // Send to colony watchers - this.io.to(`colony:${colony.id}`).emit('combat_event', eventData); - - logger.info('Colony under siege event emitted', { - correlationId, - colonyId: colony.id, - attackerFleetId, - }); - - } catch (error) { - logger.error('Failed to emit colony under siege event', { - correlationId, - colonyId: colony.id, - error: error.message, - }); - } - } - - /** - * Emit spectator joined combat event - * @param {number} battleId - Battle ID - * @param {number} spectatorCount - Current spectator count - * @param {string} correlationId - Request correlation ID - */ - emitSpectatorJoined(battleId, spectatorCount, correlationId) { - try { - const eventData = { - type: 'spectator_joined', - data: { - battleId, - spectatorCount, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Send to battle room - this.io.to(`battle:${battleId}`).emit('combat_event', eventData); - - logger.debug('Spectator joined event emitted', { - correlationId, - battleId, - spectatorCount, - }); - - } catch (error) { - logger.error('Failed to emit spectator joined event', { - correlationId, - battleId, - error: error.message, - }); - } - } - - /** - * Emit game tick completed event - * @param {number} tickNumber - Tick number - * @param {Object} metrics - Tick completion metrics - * @param {string} correlationId - Request correlation ID - */ - emitGameTickCompleted(tickNumber, metrics, correlationId) { - try { - const eventData = { - type: 'game_tick_completed', - data: { - tickNumber, - metrics: { - duration: metrics.duration, - userGroupsProcessed: metrics.userGroupsProcessed, - failedGroups: metrics.failedGroups, - totalResourcesProduced: metrics.totalResourcesProduced, - totalPlayersProcessed: metrics.totalPlayersProcessed, - completedAt: metrics.completedAt, - }, - timestamp: new Date().toISOString(), - }, - correlationId, - }; - - // Broadcast to all connected players for system status - this.io.emit('system_event', eventData); - - logger.debug('Game tick completed event emitted', { - correlationId, - tickNumber, - duration: metrics.duration, - playersProcessed: metrics.totalPlayersProcessed, - }); - - } catch (error) { - logger.error('Failed to emit game tick completed event', { - correlationId, - tickNumber, - error: error.message, - }); - } - } - - // === RESEARCH-SPECIFIC EVENTS === - - /** - * Emit research started event - * @param {number} playerId - Player ID - * @param {Object} researchData - Research data - * @param {string} correlationId - Request correlation ID - */ - emitResearchStarted(playerId, researchData, correlationId) { - try { - const eventData = { - type: 'research_started', - data: { - technology: { - id: researchData.technology_id, - name: researchData.name, - description: researchData.description, - category: researchData.category, - tier: researchData.tier, - research_time: researchData.research_time - }, - costs_paid: researchData.costs_paid, - started_at: researchData.started_at, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('research_event', eventData); - - logger.info('Research started event emitted', { - correlationId, - playerId, - technologyId: researchData.technology_id, - technologyName: researchData.name - }); - - } catch (error) { - logger.error('Failed to emit research started event', { - correlationId, - playerId, - technologyId: researchData.technology_id, - error: error.message - }); - } - } - - /** - * Emit research progress update event - * @param {number} playerId - Player ID - * @param {Object} progressData - Progress data - * @param {string} correlationId - Request correlation ID - */ - emitResearchProgress(playerId, progressData, correlationId) { - try { - const eventData = { - type: 'research_progress', - data: { - technology_id: progressData.technology_id, - progress: progressData.progress, - total_time: progressData.total_time, - completion_percentage: progressData.completion_percentage, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('research_event', eventData); - - logger.debug('Research progress event emitted', { - correlationId, - playerId, - technologyId: progressData.technology_id, - progress: progressData.progress, - completionPercentage: progressData.completion_percentage - }); - - } catch (error) { - logger.error('Failed to emit research progress event', { - correlationId, - playerId, - technologyId: progressData.technology_id, - error: error.message - }); - } - } - - /** - * Emit research completed event - * @param {number} playerId - Player ID - * @param {Object} completionData - Completion data - * @param {string} correlationId - Request correlation ID - */ - emitResearchCompleted(playerId, completionData, correlationId) { - try { - const eventData = { - type: 'research_completed', - data: { - technology: completionData.technology, - completed_at: completionData.completed_at, - research_time: completionData.research_time, - effects: completionData.technology.effects, - unlocks: completionData.technology.unlocks, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('research_event', eventData); - - // Also send as a notification for important completions - this.emitNotification(playerId, { - type: 'research_completed', - title: 'Research Completed', - message: `${completionData.technology.name} research has been completed!`, - data: { - technology_id: completionData.technology.id, - technology_name: completionData.technology.name - }, - priority: 'high' - }, correlationId); - - logger.info('Research completed event emitted', { - correlationId, - playerId, - technologyId: completionData.technology.id, - technologyName: completionData.technology.name - }); - - } catch (error) { - logger.error('Failed to emit research completed event', { - correlationId, - playerId, - technologyId: completionData.technology?.id, - error: error.message - }); - } - } - - /** - * Emit research cancelled event - * @param {number} playerId - Player ID - * @param {Object} cancelledTechnology - Cancelled technology data - * @param {string} correlationId - Request correlation ID - */ - emitResearchCancelled(playerId, cancelledTechnology, correlationId) { - try { - const eventData = { - type: 'research_cancelled', - data: { - cancelled_technology: cancelledTechnology, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('research_event', eventData); - - logger.info('Research cancelled event emitted', { - correlationId, - playerId, - technologyId: cancelledTechnology.id, - technologyName: cancelledTechnology.name, - progressLost: cancelledTechnology.progress - }); - - } catch (error) { - logger.error('Failed to emit research cancelled event', { - correlationId, - playerId, - technologyId: cancelledTechnology?.id, - error: error.message - }); - } - } - - /** - * Emit technology unlocked event (when prerequisites are met) - * @param {number} playerId - Player ID - * @param {Array} unlockedTechnologies - Array of newly unlocked technologies - * @param {string} correlationId - Request correlation ID - */ - emitTechnologyUnlocked(playerId, unlockedTechnologies, correlationId) { - try { - if (!unlockedTechnologies || unlockedTechnologies.length === 0) { - return; - } - - const eventData = { - type: 'technology_unlocked', - data: { - unlocked_technologies: unlockedTechnologies, - count: unlockedTechnologies.length, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('research_event', eventData); - - // Send notification for new unlocks - if (unlockedTechnologies.length === 1) { - this.emitNotification(playerId, { - type: 'technology_unlocked', - title: 'New Technology Available', - message: `${unlockedTechnologies[0].name} is now available for research!`, - data: { - technology_id: unlockedTechnologies[0].id, - technology_name: unlockedTechnologies[0].name - }, - priority: 'medium' - }, correlationId); - } else { - this.emitNotification(playerId, { - type: 'technology_unlocked', - title: 'New Technologies Available', - message: `${unlockedTechnologies.length} new technologies are now available for research!`, - data: { - count: unlockedTechnologies.length, - technologies: unlockedTechnologies.map(tech => ({ - id: tech.id, - name: tech.name - })) - }, - priority: 'medium' - }, correlationId); - } - - logger.info('Technology unlocked event emitted', { - correlationId, - playerId, - unlockedCount: unlockedTechnologies.length, - technologies: unlockedTechnologies.map(tech => `${tech.id}:${tech.name}`) - }); - - } catch (error) { - logger.error('Failed to emit technology unlocked event', { - correlationId, - playerId, - unlockedCount: unlockedTechnologies?.length, - error: error.message - }); - } - } - - /** - * Emit research facility built event - * @param {number} playerId - Player ID - * @param {number} colonyId - Colony ID - * @param {Object} facility - Research facility data - * @param {string} correlationId - Request correlation ID - */ - emitResearchFacilityBuilt(playerId, colonyId, facility, correlationId) { - try { - const eventData = { - type: 'research_facility_built', - data: { - colonyId, - facility: { - id: facility.id, - name: facility.name, - facility_type: facility.facility_type, - research_bonus: facility.research_bonus, - specialization: facility.specialization - }, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('research_event', eventData); - - // Send to colony watchers - this.io.to(`colony:${colonyId}`).emit('research_event', eventData); - - logger.info('Research facility built event emitted', { - correlationId, - playerId, - colonyId, - facilityId: facility.id, - facilityName: facility.name - }); - - } catch (error) { - logger.error('Failed to emit research facility built event', { - correlationId, - playerId, - colonyId, - facilityId: facility?.id, - error: error.message - }); - } - } - - // === FLEET-SPECIFIC EVENTS === - - /** - * Emit fleet created event - * @param {number} playerId - Player ID - * @param {Object} fleet - Fleet data - * @param {string} correlationId - Request correlation ID - */ - emitFleetCreated(playerId, fleet, correlationId) { - try { - const eventData = { - type: 'fleet_created', - data: { - fleet: { - id: fleet.id, - name: fleet.name, - location: fleet.current_location, - status: fleet.fleet_status, - created_at: fleet.created_at - }, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('fleet_event', eventData); - - // Send to location watchers - this.io.to(`location:${fleet.current_location}`).emit('fleet_event', eventData); - - logger.info('Fleet created event emitted', { - correlationId, - playerId, - fleetId: fleet.id, - fleetName: fleet.name, - location: fleet.current_location - }); - - } catch (error) { - logger.error('Failed to emit fleet created event', { - correlationId, - playerId, - fleetId: fleet?.id, - error: error.message - }); - } - } - - /** - * Emit fleet movement started event - * @param {number} playerId - Player ID - * @param {Object} movementData - Movement data - * @param {string} correlationId - Request correlation ID - */ - emitFleetMovementStarted(playerId, movementData, correlationId) { - try { - const eventData = { - type: 'fleet_movement_started', - data: { - fleet_id: movementData.fleet_id, - from: movementData.from, - to: movementData.to, - travel_time_minutes: movementData.travel_time_minutes, - arrival_time: movementData.arrival_time, - status: movementData.status, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('fleet_event', eventData); - - // Send to origin location watchers - this.io.to(`location:${movementData.from}`).emit('fleet_event', eventData); - - // Send to destination location watchers - if (movementData.from !== movementData.to) { - this.io.to(`location:${movementData.to}`).emit('fleet_event', eventData); - } - - logger.info('Fleet movement started event emitted', { - correlationId, - playerId, - fleetId: movementData.fleet_id, - from: movementData.from, - to: movementData.to, - travelTime: movementData.travel_time_minutes - }); - - } catch (error) { - logger.error('Failed to emit fleet movement started event', { - correlationId, - playerId, - fleetId: movementData?.fleet_id, - error: error.message - }); - } - } - - /** - * Emit fleet movement completed event - * @param {number} playerId - Player ID - * @param {Object} arrivalData - Arrival data - * @param {string} correlationId - Request correlation ID - */ - emitFleetMovementCompleted(playerId, arrivalData, correlationId) { - try { - const eventData = { - type: 'fleet_movement_completed', - data: { - fleet_id: arrivalData.fleet_id, - fleet_name: arrivalData.fleet_name, - arrived_at: arrivalData.arrived_at, - arrival_time: arrivalData.arrival_time, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('fleet_event', eventData); - - // Send to destination location watchers - this.io.to(`location:${arrivalData.arrived_at}`).emit('fleet_event', eventData); - - // Send notification for successful arrival - this.emitNotification(playerId, { - type: 'fleet_arrival', - title: 'Fleet Arrived', - message: `${arrivalData.fleet_name} has arrived at ${arrivalData.arrived_at}`, - data: { - fleet_id: arrivalData.fleet_id, - fleet_name: arrivalData.fleet_name, - location: arrivalData.arrived_at - }, - priority: 'medium' - }, correlationId); - - logger.info('Fleet movement completed event emitted', { - correlationId, - playerId, - fleetId: arrivalData.fleet_id, - fleetName: arrivalData.fleet_name, - arrivedAt: arrivalData.arrived_at - }); - - } catch (error) { - logger.error('Failed to emit fleet movement completed event', { - correlationId, - playerId, - fleetId: arrivalData?.fleet_id, - error: error.message - }); - } - } - - /** - * Emit fleet disbanded event - * @param {number} playerId - Player ID - * @param {Object} disbandData - Disband data - * @param {string} correlationId - Request correlation ID - */ - emitFleetDisbanded(playerId, disbandData, correlationId) { - try { - const eventData = { - type: 'fleet_disbanded', - data: { - fleet_id: disbandData.fleet_id, - fleet_name: disbandData.fleet_name, - ships_disbanded: disbandData.ships_disbanded, - salvage_recovered: disbandData.salvage_recovered, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('fleet_event', eventData); - - // Send notification about salvage if any was recovered - const totalSalvage = Object.values(disbandData.salvage_recovered).reduce((sum, amount) => sum + amount, 0); - if (totalSalvage > 0) { - this.emitNotification(playerId, { - type: 'fleet_salvage', - title: 'Fleet Salvaged', - message: `${disbandData.fleet_name} was disbanded and salvage materials were recovered`, - data: { - fleet_name: disbandData.fleet_name, - salvage: disbandData.salvage_recovered - }, - priority: 'low' - }, correlationId); - } - - logger.info('Fleet disbanded event emitted', { - correlationId, - playerId, - fleetId: disbandData.fleet_id, - fleetName: disbandData.fleet_name, - shipsDisbanded: disbandData.ships_disbanded, - totalSalvage: totalSalvage - }); - - } catch (error) { - logger.error('Failed to emit fleet disbanded event', { - correlationId, - playerId, - fleetId: disbandData?.fleet_id, - error: error.message - }); - } - } - - /** - * Emit fleet construction completed event - * @param {number} playerId - Player ID - * @param {Object} constructionData - Construction completion data - * @param {string} correlationId - Request correlation ID - */ - emitFleetConstructionCompleted(playerId, constructionData, correlationId) { - try { - const eventData = { - type: 'fleet_construction_completed', - data: { - fleet_id: constructionData.fleet_id, - fleet_name: constructionData.fleet_name, - location: constructionData.location, - ships_constructed: constructionData.ships_constructed, - construction_time: constructionData.construction_time, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('fleet_event', eventData); - - // Send to location watchers - this.io.to(`location:${constructionData.location}`).emit('fleet_event', eventData); - - // Send notification about completion - this.emitNotification(playerId, { - type: 'fleet_ready', - title: 'Fleet Ready', - message: `${constructionData.fleet_name} construction completed at ${constructionData.location}`, - data: { - fleet_id: constructionData.fleet_id, - fleet_name: constructionData.fleet_name, - location: constructionData.location, - ships_count: constructionData.ships_constructed - }, - priority: 'high' - }, correlationId); - - logger.info('Fleet construction completed event emitted', { - correlationId, - playerId, - fleetId: constructionData.fleet_id, - fleetName: constructionData.fleet_name, - location: constructionData.location, - shipsConstructed: constructionData.ships_constructed - }); - - } catch (error) { - logger.error('Failed to emit fleet construction completed event', { - correlationId, - playerId, - fleetId: constructionData?.fleet_id, - error: error.message - }); - } - } - - /** - * Emit ship design unlocked event - * @param {number} playerId - Player ID - * @param {Array} unlockedDesigns - Array of newly unlocked ship designs - * @param {string} correlationId - Request correlation ID - */ - emitShipDesignUnlocked(playerId, unlockedDesigns, correlationId) { - try { - if (!unlockedDesigns || unlockedDesigns.length === 0) { - return; - } - - const eventData = { - type: 'ship_design_unlocked', - data: { - unlocked_designs: unlockedDesigns, - count: unlockedDesigns.length, - timestamp: new Date().toISOString() - }, - correlationId - }; - - // Send to the player - this.io.to(`player:${playerId}`).emit('fleet_event', eventData); - - // Send notification for new ship designs - if (unlockedDesigns.length === 1) { - this.emitNotification(playerId, { - type: 'ship_design_unlocked', - title: 'New Ship Design Available', - message: `${unlockedDesigns[0].name} is now available for construction!`, - data: { - design_id: unlockedDesigns[0].id, - design_name: unlockedDesigns[0].name, - ship_class: unlockedDesigns[0].ship_class - }, - priority: 'medium' - }, correlationId); - } else { - this.emitNotification(playerId, { - type: 'ship_design_unlocked', - title: 'New Ship Designs Available', - message: `${unlockedDesigns.length} new ship designs are now available for construction!`, - data: { - count: unlockedDesigns.length, - designs: unlockedDesigns.map(design => ({ - id: design.id, - name: design.name, - ship_class: design.ship_class - })) - }, - priority: 'medium' - }, correlationId); - } - - logger.info('Ship design unlocked event emitted', { - correlationId, - playerId, - unlockedCount: unlockedDesigns.length, - designs: unlockedDesigns.map(design => `${design.id}:${design.name}`) - }); - - } catch (error) { - logger.error('Failed to emit ship design unlocked event', { - correlationId, - playerId, - unlockedCount: unlockedDesigns?.length, - error: error.message - }); - } - } - - // Helper methods for combat events - - /** - * Get defending player ID from battle participants - * @param {Object} participants - Battle participants - * @returns {Promise} Defending player ID - */ - async getDefendingPlayerId(participants) { - try { - if (participants.defender_fleet_id) { - const db = require('../../database/connection'); - const fleet = await db('fleets') - .select('player_id') - .where('id', participants.defender_fleet_id) - .first(); - return fleet?.player_id || null; - } - - if (participants.defender_colony_id) { - const db = require('../../database/connection'); - const colony = await db('colonies') - .select('player_id') - .where('id', participants.defender_colony_id) - .first(); - return colony?.player_id || null; - } - - return null; - } catch (error) { - logger.error('Failed to get defending player ID', { - participants, - error: error.message, - }); - return null; - } - } - - /** - * Get battle participants (player IDs) - * @param {number} battleId - Battle ID - * @returns {Promise} Array of participant player IDs - */ - async getBattleParticipants(battleId) { - try { - const db = require('../../database/connection'); - const battle = await db('battles') - .select('participants') - .where('id', battleId) - .first(); - - if (!battle) return []; - - const participants = JSON.parse(battle.participants); - const playerIds = []; - - if (participants.attacker_player_id) { - playerIds.push(participants.attacker_player_id); - } - - const defendingPlayerId = await this.getDefendingPlayerId(participants); - if (defendingPlayerId && !playerIds.includes(defendingPlayerId)) { - playerIds.push(defendingPlayerId); - } - - return playerIds; - } catch (error) { - logger.error('Failed to get battle participants', { - battleId, - error: error.message, - }); - return []; - } - } -} - -module.exports = GameEventService; diff --git a/src/templates/emails/README.md b/src/templates/emails/README.md deleted file mode 100644 index dd2844e..0000000 --- a/src/templates/emails/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Email Templates - -This directory contains HTML email templates for the Shattered Void MMO authentication system. - -## Template Structure - -### Base Template (`base.html`) -The base template provides: -- Consistent styling and branding -- Responsive design for mobile devices -- Dark mode considerations -- Accessibility features -- Social media links placeholder -- Unsubscribe functionality - -### Individual Templates - -#### `verification.html` -Used for email address verification during registration. -- Variables: `{{username}}`, `{{verificationUrl}}` -- Features: Game overview, verification link, security notice - -#### `password-reset.html` -Used for password reset requests. -- Variables: `{{username}}`, `{{resetUrl}}` -- Features: Security warnings, password tips, expiration notice - -#### `security-alert.html` -Used for security-related notifications. -- Variables: `{{username}}`, `{{alertType}}`, `{{timestamp}}`, `{{details}}` -- Features: Alert details, action buttons, security recommendations - -## Usage - -These templates are used by the EmailService class. The service automatically: -1. Loads the appropriate template -2. Replaces template variables with actual values -3. Generates both HTML and plain text versions -4. Handles inline styles for better email client compatibility - -## Template Variables - -Common variables available in all templates: -- `{{username}}` - Player's username -- `{{unsubscribeUrl}}` - Link to unsubscribe from emails -- `{{preferencesUrl}}` - Link to email preferences -- `{{supportUrl}}` - Link to support/help -- `{{baseUrl}}` - Application base URL - -## Customization - -To customize templates: -1. Edit the HTML files directly -2. Use `{{variableName}}` for dynamic content -3. Test with different email clients -4. Ensure mobile responsiveness -5. Maintain accessibility standards - -## Email Client Compatibility - -These templates are designed to work with: -- Gmail (web, mobile, app) -- Outlook (web, desktop, mobile) -- Apple Mail (iOS, macOS) -- Yahoo Mail -- Thunderbird -- Other major email clients - -## Security Considerations - -- All external links use HTTPS -- No JavaScript or external resources -- Inline styles for security -- Proper HTML encoding for user data -- Unsubscribe links included for compliance - -## Future Enhancements - -Planned template additions: -- Welcome email after verification -- Password change confirmation -- Account suspension/reactivation -- Game event notifications -- Newsletter templates \ No newline at end of file diff --git a/src/templates/emails/base.html b/src/templates/emails/base.html deleted file mode 100644 index 26b6e66..0000000 --- a/src/templates/emails/base.html +++ /dev/null @@ -1,247 +0,0 @@ - - - - - - - {{subject}} - Shattered Void - - - - - - \ No newline at end of file diff --git a/src/templates/emails/password-reset.html b/src/templates/emails/password-reset.html deleted file mode 100644 index 44596e2..0000000 --- a/src/templates/emails/password-reset.html +++ /dev/null @@ -1,41 +0,0 @@ -

Password Reset Request

- -

Hello {{username}},

- -

We received a request to reset your password for your Shattered Void account. If you requested this, click the button below to set a new password.

- - - -
-
⚠️ Security Notice
-

This reset link will expire in 1 hour for your security. If you need more time, you can request a new reset link.

-
- -

If the button above doesn't work, you can copy and paste this link into your browser:

- -
{{resetUrl}}
- -
-

Password Security Tips:

-
    -
  • Use a unique password that you haven't used elsewhere
  • -
  • Make it at least 12 characters long
  • -
  • Include uppercase, lowercase, numbers, and special characters
  • -
  • Consider using a password manager
  • -
-
- -
-
Didn't request this?
-

If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged, and your account is secure.

-

If you're concerned about your account security, please contact our support team immediately.

-
- -

Need help? Reply to this email or contact our support team. We're here to help you get back into the galaxy safely.

- -

- The Shattered Void Security Team
- Protecting the galaxy, one account at a time. -

\ No newline at end of file diff --git a/src/templates/emails/security-alert.html b/src/templates/emails/security-alert.html deleted file mode 100644 index 2e32858..0000000 --- a/src/templates/emails/security-alert.html +++ /dev/null @@ -1,52 +0,0 @@ -

🚨 Security Alert

- -

Hello {{username}},

- -

We detected security-related activity on your Shattered Void account that we wanted to bring to your attention.

- -
-

Alert Type: {{alertType}}

-

Time: {{timestamp}}

-

Details: {{details}}

-
- -
-
What should you do?
-

If this was you: No action is required. Your account remains secure.

-

If this was NOT you: Please take immediate action to secure your account:

-
    -
  • Change your password immediately
  • -
  • Review your recent account activity
  • -
  • Contact our support team if you need assistance
  • -
-
- - - -

Additional Security Recommendations:

-
    -
  • Enable two-factor authentication (coming soon)
  • -
  • Use a strong, unique password
  • -
  • Keep your email account secure
  • -
  • Log out from shared or public computers
  • -
  • Monitor your account activity regularly
  • -
- -
-

Account Protection: We continuously monitor for suspicious activity to keep your galactic empire safe. This alert is part of our proactive security measures.

-
- -

If you have any questions or concerns about your account security, please don't hesitate to contact our support team. We're here to help protect your presence in the galaxy.

- -

- The Shattered Void Security Team
- Your security is our priority. -

- -
-
About This Alert
-

This is an automated security notification. We send these alerts to help protect your account from unauthorized access. If you have security concerns, please contact support immediately.

-
\ No newline at end of file diff --git a/src/templates/emails/verification.html b/src/templates/emails/verification.html deleted file mode 100644 index 285937f..0000000 --- a/src/templates/emails/verification.html +++ /dev/null @@ -1,37 +0,0 @@ -

Welcome to Shattered Void, {{username}}!

- -

Thank you for joining our post-collapse galaxy. To complete your registration and begin rebuilding civilization, please verify your email address.

- - - -
-

Important: This verification link will expire in 24 hours for security reasons.

-
- -

If the button above doesn't work, you can copy and paste this link into your browser:

- -
{{verificationUrl}}
- -

Once your email is verified, you'll be able to:

-
    -
  • 🏗️ Establish your first colony
  • -
  • ⚡ Manage resources and energy systems
  • -
  • 🚀 Build and command fleets
  • -
  • 🔬 Research new technologies
  • -
  • 🤝 Form alliances with other survivors
  • -
  • ⚔️ Engage in strategic combat
  • -
- -
-
Didn't create an account?
-

If you didn't register for Shattered Void, you can safely ignore this email. No account will be created without email verification.

-
- -

Welcome to the galaxy, Commander. The future of civilization rests in your hands.

- -

- The Shattered Void Team
- "From the ashes, we rise." -

\ No newline at end of file diff --git a/src/tests/helpers/test-helpers.js b/src/tests/helpers/test-helpers.js deleted file mode 100644 index 8b7fe67..0000000 --- a/src/tests/helpers/test-helpers.js +++ /dev/null @@ -1,479 +0,0 @@ -/** - * Test Helpers - * Utility functions for setting up test data - */ - -const db = require('../../database/connection'); -const jwt = require('jsonwebtoken'); -const bcrypt = require('bcrypt'); - -/** - * Create a test user with authentication token - * @param {string} email - User email - * @param {string} username - Username - * @param {string} password - Password (optional, defaults to 'testpassword') - * @returns {Promise} User and token - */ -async function createTestUser(email, username, password = 'testpassword') { - try { - // Hash password - const hashedPassword = await bcrypt.hash(password, 10); - - // Create user - const [user] = await db('players').insert({ - email, - username, - password_hash: hashedPassword, - email_verified: true, - user_group: Math.floor(Math.random() * 10), - is_active: true, - created_at: new Date(), - updated_at: new Date(), - }).returning('*'); - - // Generate JWT token - const token = jwt.sign( - { id: user.id, username: user.username }, - process.env.JWT_SECRET || 'test-secret', - { expiresIn: '24h' }, - ); - - // Initialize player resources - const resourceTypes = await db('resource_types').where('is_active', true); - for (const resourceType of resourceTypes) { - await db('player_resources').insert({ - player_id: user.id, - resource_type_id: resourceType.id, - amount: 10000, // Generous amount for testing - storage_capacity: 50000, - last_updated: new Date(), - }); - } - - // Initialize combat statistics - await db('combat_statistics').insert({ - player_id: user.id, - battles_initiated: 0, - battles_won: 0, - battles_lost: 0, - ships_lost: 0, - ships_destroyed: 0, - total_damage_dealt: 0, - total_damage_received: 0, - total_experience_gained: 0, - resources_looted: JSON.stringify({}), - created_at: new Date(), - updated_at: new Date(), - }); - - return { user, token }; - } catch (error) { - console.error('Failed to create test user:', error); - throw error; - } -} - -/** - * Create a test fleet with ships - * @param {number} playerId - Owner player ID - * @param {string} name - Fleet name - * @param {string} location - Fleet location - * @returns {Promise} Created fleet - */ -async function createTestFleet(playerId, name, location) { - try { - // Get or create a basic ship design - let shipDesign = await db('ship_designs') - .where('name', 'Test Fighter') - .where('is_public', true) - .first(); - - if (!shipDesign) { - [shipDesign] = await db('ship_designs').insert({ - name: 'Test Fighter', - ship_class: 'fighter', - hull_type: 'light', - components: JSON.stringify({ - weapons: ['basic_laser'], - shields: ['basic_shield'], - engines: ['basic_engine'], - }), - stats: JSON.stringify({ - hp: 100, - attack: 15, - defense: 10, - speed: 5, - }), - cost: JSON.stringify({ - scrap: 100, - energy: 50, - }), - build_time: 30, - is_public: true, - is_active: true, - hull_points: 100, - shield_points: 25, - armor_points: 10, - attack_power: 15, - attack_speed: 1.0, - movement_speed: 5, - cargo_capacity: 0, - special_abilities: JSON.stringify([]), - damage_resistances: JSON.stringify({}), - created_at: new Date(), - updated_at: new Date(), - }).returning('*'); - } - - // Create fleet - const [fleet] = await db('fleets').insert({ - player_id: playerId, - name, - current_location: location, - destination: null, - fleet_status: 'idle', - combat_rating: 150, - total_ship_count: 10, - fleet_composition: JSON.stringify({ - 'Test Fighter': 10, - }), - combat_victories: 0, - combat_defeats: 0, - movement_started: null, - arrival_time: null, - last_updated: new Date(), - created_at: new Date(), - }).returning('*'); - - // Add ships to fleet - await db('fleet_ships').insert({ - fleet_id: fleet.id, - ship_design_id: shipDesign.id, - quantity: 10, - health_percentage: 100, - experience: 0, - created_at: new Date(), - }); - - // Create initial combat experience record - await db('ship_combat_experience').insert({ - fleet_id: fleet.id, - ship_design_id: shipDesign.id, - battles_survived: 0, - enemies_destroyed: 0, - damage_dealt: 0, - experience_points: 0, - veterancy_level: 1, - combat_bonuses: JSON.stringify({}), - created_at: new Date(), - updated_at: new Date(), - }); - - return fleet; - } catch (error) { - console.error('Failed to create test fleet:', error); - throw error; - } -} - -/** - * Create a test colony with basic buildings - * @param {number} playerId - Owner player ID - * @param {string} name - Colony name - * @param {string} coordinates - Colony coordinates - * @param {number} planetTypeId - Planet type ID (optional) - * @returns {Promise} Created colony - */ -async function createTestColony(playerId, name, coordinates, planetTypeId = null) { - try { - // Get planet type - if (!planetTypeId) { - const planetType = await db('planet_types') - .where('is_active', true) - .first(); - planetTypeId = planetType?.id || 1; - } - - // Get sector - const sectorCoordinates = coordinates.split('-').slice(0, 2).join('-'); - let sector = await db('galaxy_sectors') - .where('coordinates', sectorCoordinates) - .first(); - - if (!sector) { - [sector] = await db('galaxy_sectors').insert({ - name: `Test Sector ${sectorCoordinates}`, - coordinates: sectorCoordinates, - description: 'Test sector for integration tests', - danger_level: 3, - special_rules: JSON.stringify({}), - created_at: new Date(), - }).returning('*'); - } - - // Create colony - const [colony] = await db('colonies').insert({ - player_id: playerId, - name, - coordinates, - sector_id: sector.id, - planet_type_id: planetTypeId, - population: 1000, - max_population: 10000, - morale: 100, - loyalty: 100, - defense_rating: 50, - shield_strength: 25, - under_siege: false, - successful_defenses: 0, - times_captured: 0, - founded_at: new Date(), - last_updated: new Date(), - }).returning('*'); - - // Add basic buildings - const commandCenter = await db('building_types') - .where('name', 'Command Center') - .first(); - - if (commandCenter) { - await db('colony_buildings').insert({ - colony_id: colony.id, - building_type_id: commandCenter.id, - level: 1, - health_percentage: 100, - is_under_construction: false, - created_at: new Date(), - updated_at: new Date(), - }); - } - - // Add defense grid for combat testing - const defenseGrid = await db('building_types') - .where('name', 'Defense Grid') - .first(); - - if (defenseGrid) { - await db('colony_buildings').insert({ - colony_id: colony.id, - building_type_id: defenseGrid.id, - level: 2, - health_percentage: 100, - is_under_construction: false, - created_at: new Date(), - updated_at: new Date(), - }); - } - - // Initialize colony resource production - const resourceTypes = await db('resource_types').where('is_active', true); - for (const resourceType of resourceTypes) { - await db('colony_resource_production').insert({ - colony_id: colony.id, - resource_type_id: resourceType.id, - production_rate: 10, - consumption_rate: 5, - current_stored: 1000, - storage_capacity: 10000, - last_calculated: new Date(), - }); - } - - return colony; - } catch (error) { - console.error('Failed to create test colony:', error); - throw error; - } -} - -/** - * Create test combat configuration - * @param {string} name - Configuration name - * @param {string} type - Combat type - * @param {Object} config - Configuration data - * @returns {Promise} Created configuration - */ -async function createTestCombatConfig(name, type, config = {}) { - try { - const [combatConfig] = await db('combat_configurations').insert({ - config_name: name, - combat_type: type, - config_data: JSON.stringify({ - auto_resolve: true, - preparation_time: 1, - max_rounds: 5, - round_duration: 2, - damage_variance: 0.1, - experience_gain: 1.0, - casualty_rate_min: 0.1, - casualty_rate_max: 0.7, - loot_multiplier: 1.0, - spectator_limit: 100, - priority: 100, - ...config, - }), - description: `Test ${type} combat configuration`, - is_active: true, - created_at: new Date(), - updated_at: new Date(), - }).returning('*'); - - return combatConfig; - } catch (error) { - console.error('Failed to create test combat config:', error); - throw error; - } -} - -/** - * Create a completed combat encounter for testing history - * @param {number} attackerFleetId - Attacker fleet ID - * @param {number} defenderFleetId - Defender fleet ID (optional) - * @param {number} defenderColonyId - Defender colony ID (optional) - * @param {string} outcome - Combat outcome - * @returns {Promise} Created encounter - */ -async function createTestCombatEncounter(attackerFleetId, defenderFleetId = null, defenderColonyId = null, outcome = 'attacker_victory') { - try { - // Create battle - const [battle] = await db('battles').insert({ - battle_type: defenderColonyId ? 'fleet_vs_colony' : 'fleet_vs_fleet', - location: 'A3-91-X', - combat_type_id: 1, - participants: JSON.stringify({ - attacker_fleet_id: attackerFleetId, - defender_fleet_id: defenderFleetId, - defender_colony_id: defenderColonyId, - }), - status: 'completed', - battle_data: JSON.stringify({}), - result: JSON.stringify({ outcome }), - started_at: new Date(Date.now() - 300000), // 5 minutes ago - completed_at: new Date(), - created_at: new Date(), - }).returning('*'); - - // Create encounter - const [encounter] = await db('combat_encounters').insert({ - battle_id: battle.id, - attacker_fleet_id: attackerFleetId, - defender_fleet_id: defenderFleetId, - defender_colony_id: defenderColonyId, - encounter_type: battle.battle_type, - location: battle.location, - initial_forces: JSON.stringify({ - attacker: { ships: 10 }, - defender: { ships: 8 }, - }), - final_forces: JSON.stringify({ - attacker: { ships: outcome === 'attacker_victory' ? 7 : 2 }, - defender: { ships: outcome === 'defender_victory' ? 6 : 0 }, - }), - casualties: JSON.stringify({ - attacker: { ships: {}, total_ships: outcome === 'attacker_victory' ? 3 : 8 }, - defender: { ships: {}, total_ships: outcome === 'defender_victory' ? 2 : 8 }, - }), - combat_log: JSON.stringify([ - { round: 1, event: 'combat_start', description: 'Combat initiated' }, - { round: 1, event: 'combat_resolution', description: `${outcome.replace('_', ' ')}` }, - ]), - experience_gained: 100, - loot_awarded: JSON.stringify(outcome === 'attacker_victory' ? { scrap: 500, energy: 250 } : {}), - outcome, - duration_seconds: 90, - started_at: battle.started_at, - completed_at: battle.completed_at, - created_at: new Date(), - }).returning('*'); - - return { battle, encounter }; - } catch (error) { - console.error('Failed to create test combat encounter:', error); - throw error; - } -} - -/** - * Wait for a condition to be met - * @param {Function} condition - Function that returns true when condition is met - * @param {number} timeout - Timeout in milliseconds - * @param {number} interval - Check interval in milliseconds - * @returns {Promise} - */ -async function waitForCondition(condition, timeout = 5000, interval = 100) { - const start = Date.now(); - - while (Date.now() - start < timeout) { - if (await condition()) { - return; - } - await new Promise(resolve => setTimeout(resolve, interval)); - } - - throw new Error(`Condition not met within ${timeout}ms`); -} - -/** - * Clean up all test data - */ -async function cleanupTestData() { - try { - // Delete in order to respect foreign key constraints - await db('combat_logs').del(); - await db('combat_encounters').del(); - await db('combat_queue').del(); - await db('battles').del(); - await db('ship_combat_experience').del(); - await db('fleet_ships').del(); - await db('fleet_positions').del(); - await db('fleets').del(); - await db('colony_resource_production').del(); - await db('colony_buildings').del(); - await db('colonies').del(); - await db('player_resources').del(); - await db('combat_statistics').del(); - await db('players').where('email', 'like', '%@test.com').del(); - await db('combat_configurations').where('config_name', 'like', 'test_%').del(); - await db('ship_designs').where('name', 'like', 'Test %').del(); - await db('galaxy_sectors').where('name', 'like', 'Test Sector%').del(); - } catch (error) { - console.error('Failed to cleanup test data:', error); - } -} - -/** - * Reset combat-related data between tests - */ -async function resetCombatData() { - try { - await db('combat_logs').del(); - await db('combat_encounters').del(); - await db('combat_queue').del(); - await db('battles').del(); - - // Reset fleet statuses - await db('fleets').update({ - fleet_status: 'idle', - last_combat: null, - }); - - // Reset colony siege status - await db('colonies').update({ - under_siege: false, - last_attacked: null, - }); - } catch (error) { - console.error('Failed to reset combat data:', error); - } -} - -module.exports = { - createTestUser, - createTestFleet, - createTestColony, - createTestCombatConfig, - createTestCombatEncounter, - waitForCondition, - cleanupTestData, - resetCombatData, -}; diff --git a/src/tests/integration/auth-enhanced.integration.test.js b/src/tests/integration/auth-enhanced.integration.test.js deleted file mode 100644 index c97b9f3..0000000 --- a/src/tests/integration/auth-enhanced.integration.test.js +++ /dev/null @@ -1,612 +0,0 @@ -/** - * Enhanced Authentication Integration Tests - * Tests the complete authentication flow including email verification, password reset, and security features - */ - -const request = require('supertest'); -const app = require('../../app'); -const db = require('../../database/connection'); -const redis = require('../../utils/redis'); -const EmailService = require('../../services/auth/EmailService'); -const TokenService = require('../../services/auth/TokenService'); - -describe('Enhanced Authentication Integration Tests', () => { - let testPlayer; - let authToken; - let emailService; - let tokenService; - - beforeAll(async () => { - // Initialize services - emailService = new EmailService(); - tokenService = new TokenService(); - - // Clean up any existing test data - await db('players').where('email', 'like', '%test-auth%').del(); - }); - - afterAll(async () => { - // Clean up test data - if (testPlayer) { - await db('players').where('id', testPlayer.id).del(); - } - - // Close connections - await db.destroy(); - // Redis connection cleanup handled by Redis client - }); - - beforeEach(async () => { - // Clear any existing test data - await db('players').where('email', 'like', '%test-auth%').del(); - }); - - describe('Player Registration with Email Verification', () => { - it('should register a new player and send verification email', async () => { - const registrationData = { - email: 'test-auth-register@example.com', - username: 'testAuthUser', - password: 'SecurePassword123!', - acceptTerms: true, - }; - - const response = await request(app) - .post('/api/auth/register') - .send(registrationData) - .expect(201); - - expect(response.body.success).toBe(true); - expect(response.body.data.player).toHaveProperty('id'); - expect(response.body.data.player.email).toBe(registrationData.email); - expect(response.body.data.player.username).toBe(registrationData.username); - expect(response.body.data.player.isVerified).toBe(false); - expect(response.body.data.verificationEmailSent).toBe(true); - - // Verify player was created in database - const dbPlayer = await db('players') - .where('email', registrationData.email) - .first(); - - expect(dbPlayer).toBeTruthy(); - expect(dbPlayer.email_verified).toBe(false); - expect(dbPlayer.is_active).toBe(true); - - testPlayer = response.body.data.player; - }); - - it('should reject registration with weak password', async () => { - const registrationData = { - email: 'test-auth-weak@example.com', - username: 'testWeakPassword', - password: '123', - acceptTerms: true, - }; - - const response = await request(app) - .post('/api/auth/register') - .send(registrationData) - .expect(400); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe('WEAK_PASSWORD'); - expect(response.body.details.errors).toBeDefined(); - }); - - it('should reject duplicate email registration', async () => { - const registrationData = { - email: 'test-auth-duplicate@example.com', - username: 'testDuplicate1', - password: 'SecurePassword123!', - acceptTerms: true, - }; - - // First registration - await request(app) - .post('/api/auth/register') - .send(registrationData) - .expect(201); - - // Duplicate registration - const duplicateData = { - ...registrationData, - username: 'testDuplicate2', - }; - - const response = await request(app) - .post('/api/auth/register') - .send(duplicateData) - .expect(409); - - expect(response.body.success).toBe(false); - expect(response.body.code).toBe('UNIQUENESS_ERROR'); - }); - }); - - describe('Email Verification Process', () => { - beforeEach(async () => { - // Create an unverified test player - const registrationData = { - email: 'test-auth-verify@example.com', - username: 'testVerifyUser', - password: 'SecurePassword123!', - acceptTerms: true, - }; - - const response = await request(app) - .post('/api/auth/register') - .send(registrationData); - - testPlayer = response.body.data.player; - }); - - it('should verify email with valid token', async () => { - // Generate verification token - const verificationToken = await tokenService.generateEmailVerificationToken( - testPlayer.id, - testPlayer.email - ); - - const response = await request(app) - .post('/api/auth/verify-email') - .send({ token: verificationToken }) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data.player.isVerified).toBe(true); - - // Verify in database - const dbPlayer = await db('players') - .where('id', testPlayer.id) - .first(); - - expect(dbPlayer.email_verified).toBe(true); - }); - - it('should reject invalid verification token', async () => { - const invalidToken = 'a'.repeat(64); // Invalid hex token - - const response = await request(app) - .post('/api/auth/verify-email') - .send({ token: invalidToken }) - .expect(400); - - expect(response.body.success).toBe(false); - }); - - it('should resend verification email', async () => { - const response = await request(app) - .post('/api/auth/resend-verification') - .send({ email: testPlayer.email }) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.message).toContain('verification email sent'); - }); - }); - - describe('Login with Security Features', () => { - beforeEach(async () => { - // Create and verify a test player - const registrationData = { - email: 'test-auth-login@example.com', - username: 'testLoginUser', - password: 'SecurePassword123!', - acceptTerms: true, - }; - - const regResponse = await request(app) - .post('/api/auth/register') - .send(registrationData); - - testPlayer = regResponse.body.data.player; - - // Verify the player - const verificationToken = await tokenService.generateEmailVerificationToken( - testPlayer.id, - testPlayer.email - ); - - await request(app) - .post('/api/auth/verify-email') - .send({ token: verificationToken }); - }); - - it('should login successfully with valid credentials', async () => { - const loginData = { - email: 'test-auth-login@example.com', - password: 'SecurePassword123!', - }; - - const response = await request(app) - .post('/api/auth/login') - .send(loginData) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data.accessToken).toBeDefined(); - expect(response.body.data.player.isVerified).toBe(true); - - authToken = response.body.data.accessToken; - }); - - it('should reject login with invalid password', async () => { - const loginData = { - email: 'test-auth-login@example.com', - password: 'WrongPassword123!', - }; - - const response = await request(app) - .post('/api/auth/login') - .send(loginData) - .expect(401); - - expect(response.body.success).toBe(false); - }); - - it('should track failed login attempts', async () => { - const loginData = { - email: 'test-auth-login@example.com', - password: 'WrongPassword123!', - }; - - // Make multiple failed attempts - for (let i = 0; i < 3; i++) { - await request(app) - .post('/api/auth/login') - .send(loginData) - .expect(401); - } - - // Check if attempts are being tracked (this would require checking Redis directly) - const failedAttempts = await tokenService.isAccountLocked(loginData.email); - // Note: In a real test, you might want to check the actual lockout status - }); - }); - - describe('Password Reset Process', () => { - beforeEach(async () => { - // Create a verified test player - const registrationData = { - email: 'test-auth-reset@example.com', - username: 'testResetUser', - password: 'OriginalPassword123!', - acceptTerms: true, - }; - - const regResponse = await request(app) - .post('/api/auth/register') - .send(registrationData); - - testPlayer = regResponse.body.data.player; - - // Verify the player - const verificationToken = await tokenService.generateEmailVerificationToken( - testPlayer.id, - testPlayer.email - ); - - await request(app) - .post('/api/auth/verify-email') - .send({ token: verificationToken }); - }); - - it('should request password reset successfully', async () => { - const response = await request(app) - .post('/api/auth/request-password-reset') - .send({ email: 'test-auth-reset@example.com' }) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.message).toContain('password reset email has been sent'); - }); - - it('should reset password with valid token', async () => { - // Generate reset token - const resetToken = await tokenService.generatePasswordResetToken( - testPlayer.id, - testPlayer.email - ); - - const resetData = { - token: resetToken, - newPassword: 'NewSecurePassword123!', - confirmPassword: 'NewSecurePassword123!', - }; - - const response = await request(app) - .post('/api/auth/reset-password') - .send(resetData) - .expect(200); - - expect(response.body.success).toBe(true); - - // Verify can login with new password - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: 'test-auth-reset@example.com', - password: 'NewSecurePassword123!', - }) - .expect(200); - - expect(loginResponse.body.success).toBe(true); - }); - }); - - describe('Password Change (Authenticated)', () => { - beforeEach(async () => { - // Create, verify, and login test player - const registrationData = { - email: 'test-auth-change@example.com', - username: 'testChangeUser', - password: 'OriginalPassword123!', - acceptTerms: true, - }; - - const regResponse = await request(app) - .post('/api/auth/register') - .send(registrationData); - - testPlayer = regResponse.body.data.player; - - // Verify the player - const verificationToken = await tokenService.generateEmailVerificationToken( - testPlayer.id, - testPlayer.email - ); - - await request(app) - .post('/api/auth/verify-email') - .send({ token: verificationToken }); - - // Login to get auth token - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: 'test-auth-change@example.com', - password: 'OriginalPassword123!', - }); - - authToken = loginResponse.body.data.accessToken; - }); - - it('should change password successfully', async () => { - const changeData = { - currentPassword: 'OriginalPassword123!', - newPassword: 'NewPassword123!', - confirmPassword: 'NewPassword123!', - }; - - const response = await request(app) - .post('/api/auth/change-password') - .set('Authorization', `Bearer ${authToken}`) - .send(changeData) - .expect(200); - - expect(response.body.success).toBe(true); - - // Verify can login with new password - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: 'test-auth-change@example.com', - password: 'NewPassword123!', - }) - .expect(200); - - expect(loginResponse.body.success).toBe(true); - }); - - it('should reject password change with wrong current password', async () => { - const changeData = { - currentPassword: 'WrongCurrentPassword!', - newPassword: 'NewPassword123!', - confirmPassword: 'NewPassword123!', - }; - - const response = await request(app) - .post('/api/auth/change-password') - .set('Authorization', `Bearer ${authToken}`) - .send(changeData) - .expect(401); - - expect(response.body.success).toBe(false); - }); - }); - - describe('Token Refresh and Security', () => { - beforeEach(async () => { - // Create, verify, and login test player - const registrationData = { - email: 'test-auth-token@example.com', - username: 'testTokenUser', - password: 'SecurePassword123!', - acceptTerms: true, - }; - - const regResponse = await request(app) - .post('/api/auth/register') - .send(registrationData); - - testPlayer = regResponse.body.data.player; - - // Verify the player - const verificationToken = await tokenService.generateEmailVerificationToken( - testPlayer.id, - testPlayer.email - ); - - await request(app) - .post('/api/auth/verify-email') - .send({ token: verificationToken }); - }); - - it('should refresh access token successfully', async () => { - // Login to get refresh token - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: 'test-auth-token@example.com', - password: 'SecurePassword123!', - }); - - // Extract refresh token from cookie - const cookies = loginResponse.headers['set-cookie']; - const refreshTokenCookie = cookies.find(cookie => cookie.includes('refreshToken')); - - expect(refreshTokenCookie).toBeDefined(); - - // Use refresh token to get new access token - const refreshResponse = await request(app) - .post('/api/auth/refresh') - .set('Cookie', refreshTokenCookie) - .expect(200); - - expect(refreshResponse.body.success).toBe(true); - expect(refreshResponse.body.data.accessToken).toBeDefined(); - }); - - it('should logout and blacklist token', async () => { - // Login first - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: 'test-auth-token@example.com', - password: 'SecurePassword123!', - }); - - authToken = loginResponse.body.data.accessToken; - - // Logout - const logoutResponse = await request(app) - .post('/api/auth/logout') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(logoutResponse.body.success).toBe(true); - - // Try to use the token after logout (should fail) - const profileResponse = await request(app) - .get('/api/auth/me') - .set('Authorization', `Bearer ${authToken}`) - .expect(401); - - expect(profileResponse.body.success).toBe(false); - }); - }); - - describe('Security Status and Utilities', () => { - beforeEach(async () => { - // Create, verify, and login test player - const registrationData = { - email: 'test-auth-security@example.com', - username: 'testSecurityUser', - password: 'SecurePassword123!', - acceptTerms: true, - }; - - const regResponse = await request(app) - .post('/api/auth/register') - .send(registrationData); - - testPlayer = regResponse.body.data.player; - - // Verify and login - const verificationToken = await tokenService.generateEmailVerificationToken( - testPlayer.id, - testPlayer.email - ); - - await request(app) - .post('/api/auth/verify-email') - .send({ token: verificationToken }); - - const loginResponse = await request(app) - .post('/api/auth/login') - .send({ - email: 'test-auth-security@example.com', - password: 'SecurePassword123!', - }); - - authToken = loginResponse.body.data.accessToken; - }); - - it('should get security status', async () => { - const response = await request(app) - .get('/api/auth/security-status') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data.securityStatus).toHaveProperty('emailVerified', true); - expect(response.body.data.securityStatus).toHaveProperty('accountActive', true); - expect(response.body.data.securityStatus).toHaveProperty('accountBanned', false); - }); - - it('should check password strength', async () => { - const strongPassword = 'VerySecurePassword123!@#'; - const weakPassword = '123'; - - // Test strong password - const strongResponse = await request(app) - .post('/api/auth/check-password-strength') - .send({ password: strongPassword }) - .expect(200); - - expect(strongResponse.body.success).toBe(true); - expect(strongResponse.body.data.isValid).toBe(true); - expect(strongResponse.body.data.strength.level).toBe('excellent'); - - // Test weak password - const weakResponse = await request(app) - .post('/api/auth/check-password-strength') - .send({ password: weakPassword }) - .expect(200); - - expect(weakResponse.body.success).toBe(true); - expect(weakResponse.body.data.isValid).toBe(false); - expect(weakResponse.body.data.errors.length).toBeGreaterThan(0); - }); - }); - - describe('Rate Limiting', () => { - it('should enforce rate limits on registration', async () => { - const registrationData = { - email: 'test-auth-rate-limit@example.com', - username: 'testRateLimit', - password: 'SecurePassword123!', - acceptTerms: true, - }; - - // Make multiple rapid registration attempts - const promises = []; - for (let i = 0; i < 5; i++) { - const data = { ...registrationData, email: `test-rate-${i}@example.com` }; - promises.push( - request(app) - .post('/api/auth/register') - .send(data) - ); - } - - const responses = await Promise.all(promises); - - // Some requests should succeed, others might be rate limited - const successCount = responses.filter(r => r.status === 201).length; - const rateLimitedCount = responses.filter(r => r.status === 429).length; - - // At least some should succeed, but we might hit rate limits - expect(successCount).toBeGreaterThan(0); - }); - }); -}); - -// Mock email service to prevent actual emails in tests -jest.mock('../../services/auth/EmailService', () => { - return jest.fn().mockImplementation(() => ({ - sendEmailVerification: jest.fn().mockResolvedValue({ success: true, messageId: 'test-123' }), - sendPasswordReset: jest.fn().mockResolvedValue({ success: true, messageId: 'test-456' }), - sendSecurityAlert: jest.fn().mockResolvedValue({ success: true, messageId: 'test-789' }), - healthCheck: jest.fn().mockResolvedValue(true), - })); -}); \ No newline at end of file diff --git a/src/tests/integration/combat/combat.integration.test.js b/src/tests/integration/combat/combat.integration.test.js deleted file mode 100644 index 0f4a0f8..0000000 --- a/src/tests/integration/combat/combat.integration.test.js +++ /dev/null @@ -1,542 +0,0 @@ -/** - * Combat System Integration Tests - * Tests complete combat flows from API to database - */ - -const request = require('supertest'); -const app = require('../../../app'); -const db = require('../../../database/connection'); -const { createTestUser, createTestFleet, createTestColony, cleanupTestData } = require('../../helpers/test-helpers'); - -describe('Combat System Integration', () => { - let authToken; - let testPlayer; - let attackerFleet; - let defenderFleet; - let defenderColony; - let defenderPlayer; - - beforeAll(async () => { - // Run migrations and seeds if needed - try { - await db.migrate.latest(); - } catch (error) { - // Migrations might already be up to date - } - - // Create test players - const playerResult = await createTestUser('attacker@test.com', 'AttackerPlayer'); - testPlayer = playerResult.user; - authToken = playerResult.token; - - const defenderResult = await createTestUser('defender@test.com', 'DefenderPlayer'); - defenderPlayer = defenderResult.user; - - // Create test fleets - attackerFleet = await createTestFleet(testPlayer.id, 'Attack Fleet', 'A3-91-X'); - defenderFleet = await createTestFleet(defenderPlayer.id, 'Defense Fleet', 'A3-91-X'); - - // Create test colony - defenderColony = await createTestColony(defenderPlayer.id, 'Defended Colony', 'A3-91-Y'); - - // Initialize combat configurations - await db('combat_configurations').insert({ - config_name: 'test_instant', - combat_type: 'instant', - config_data: JSON.stringify({ - auto_resolve: true, - preparation_time: 1, // 1 second for testing - damage_variance: 0.1, - experience_gain: 1.0, - }), - is_active: true, - description: 'Test instant combat configuration', - }); - }); - - afterAll(async () => { - await cleanupTestData(); - await db.destroy(); - }); - - beforeEach(async () => { - // Reset fleet statuses - await db('fleets') - .whereIn('id', [attackerFleet.id, defenderFleet.id]) - .update({ - fleet_status: 'idle', - last_updated: new Date(), - }); - - // Reset colony siege status - await db('colonies') - .where('id', defenderColony.id) - .update({ - under_siege: false, - last_updated: new Date(), - }); - - // Clean up previous battles - await db('combat_queue').del(); - await db('combat_logs').del(); - await db('combat_encounters').del(); - await db('battles').del(); - }); - - describe('Fleet vs Fleet Combat', () => { - it('should successfully initiate and resolve fleet vs fleet combat', async () => { - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_fleet_id: defenderFleet.id, - location: 'A3-91-X', - combat_type: 'instant', - }; - - // Initiate combat - const response = await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData) - .expect(201); - - expect(response.body.success).toBe(true); - expect(response.body.data).toHaveProperty('battleId'); - expect(response.body.data.status).toBe('preparing'); - - const battleId = response.body.data.battleId; - - // Wait for auto-resolution - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Check battle was resolved - const battle = await db('battles') - .where('id', battleId) - .first(); - - expect(battle.status).toBe('completed'); - expect(battle.result).toBeDefined(); - - const result = JSON.parse(battle.result); - expect(['attacker_victory', 'defender_victory', 'draw']).toContain(result.outcome); - - // Check combat encounter was created - const encounter = await db('combat_encounters') - .where('battle_id', battleId) - .first(); - - expect(encounter).toBeDefined(); - expect(encounter.attacker_fleet_id).toBe(attackerFleet.id); - expect(encounter.defender_fleet_id).toBe(defenderFleet.id); - expect(encounter.outcome).toBe(result.outcome); - - // Check fleets returned to idle status - const updatedFleets = await db('fleets') - .whereIn('id', [attackerFleet.id, defenderFleet.id]); - - updatedFleets.forEach(fleet => { - expect(['idle', 'destroyed']).toContain(fleet.fleet_status); - }); - }); - - it('should track combat statistics', async () => { - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_fleet_id: defenderFleet.id, - location: 'A3-91-X', - combat_type: 'instant', - }; - - await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData) - .expect(201); - - // Wait for resolution - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Check combat statistics were updated - const attackerStats = await db('combat_statistics') - .where('player_id', testPlayer.id) - .first(); - - expect(attackerStats).toBeDefined(); - expect(attackerStats.battles_initiated).toBe(1); - expect(attackerStats.battles_won + attackerStats.battles_lost).toBe(1); - - const defenderStats = await db('combat_statistics') - .where('player_id', defenderPlayer.id) - .first(); - - expect(defenderStats).toBeDefined(); - expect(defenderStats.battles_won + defenderStats.battles_lost).toBe(1); - }); - - it('should prevent combat with own fleet', async () => { - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_fleet_id: attackerFleet.id, // Same fleet - location: 'A3-91-X', - combat_type: 'instant', - }; - - const response = await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData) - .expect(400); - - expect(response.body.error).toContain('Cannot attack your own fleet'); - }); - - it('should prevent combat when fleet not at location', async () => { - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_fleet_id: defenderFleet.id, - location: 'B2-50-Y', // Different location - combat_type: 'instant', - }; - - const response = await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData) - .expect(400); - - expect(response.body.error).toContain('Fleet must be at the specified location'); - }); - }); - - describe('Fleet vs Colony Combat', () => { - it('should successfully initiate and resolve fleet vs colony combat', async () => { - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_colony_id: defenderColony.id, - location: 'A3-91-Y', - combat_type: 'instant', - }; - - // Update attacker fleet location - await db('fleets') - .where('id', attackerFleet.id) - .update({ current_location: 'A3-91-Y' }); - - const response = await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData) - .expect(201); - - expect(response.body.success).toBe(true); - const battleId = response.body.data.battleId; - - // Wait for auto-resolution - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Check colony siege status was updated during combat - const encounter = await db('combat_encounters') - .where('battle_id', battleId) - .first(); - - expect(encounter).toBeDefined(); - expect(encounter.attacker_fleet_id).toBe(attackerFleet.id); - expect(encounter.defender_colony_id).toBe(defenderColony.id); - - // Check colony is no longer under siege after combat - const updatedColony = await db('colonies') - .where('id', defenderColony.id) - .first(); - - expect(updatedColony.under_siege).toBe(false); - }); - - it('should award bonus loot for successful colony raids', async () => { - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_colony_id: defenderColony.id, - location: 'A3-91-Y', - combat_type: 'instant', - }; - - await db('fleets') - .where('id', attackerFleet.id) - .update({ current_location: 'A3-91-Y' }); - - const response = await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData) - .expect(201); - - const battleId = response.body.data.battleId; - - // Wait for resolution - await new Promise(resolve => setTimeout(resolve, 2000)); - - const encounter = await db('combat_encounters') - .where('battle_id', battleId) - .first(); - - if (encounter.outcome === 'attacker_victory') { - const loot = JSON.parse(encounter.loot_awarded); - expect(loot).toHaveProperty('data_cores'); - expect(loot.data_cores).toBeGreaterThan(0); - } - }); - }); - - describe('Combat History and Statistics', () => { - beforeEach(async () => { - // Create some combat history - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_fleet_id: defenderFleet.id, - location: 'A3-91-X', - combat_type: 'instant', - }; - - await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData); - - // Wait for resolution - await new Promise(resolve => setTimeout(resolve, 2000)); - }); - - it('should retrieve combat history', async () => { - const response = await request(app) - .get('/api/combat/history') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data).toHaveProperty('combats'); - expect(response.body.data).toHaveProperty('pagination'); - expect(response.body.data.combats.length).toBeGreaterThan(0); - }); - - it('should filter combat history by outcome', async () => { - const response = await request(app) - .get('/api/combat/history?outcome=attacker_victory') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.success).toBe(true); - response.body.data.combats.forEach(combat => { - expect(combat.outcome).toBe('attacker_victory'); - }); - }); - - it('should retrieve combat statistics', async () => { - const response = await request(app) - .get('/api/combat/statistics') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data).toHaveProperty('battles_initiated'); - expect(response.body.data).toHaveProperty('battles_won'); - expect(response.body.data).toHaveProperty('battles_lost'); - expect(response.body.data).toHaveProperty('derived_stats'); - expect(response.body.data.derived_stats).toHaveProperty('win_rate_percentage'); - }); - - it('should retrieve active combats', async () => { - // Initiate combat but don't wait for resolution - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_fleet_id: defenderFleet.id, - location: 'A3-91-X', - combat_type: 'instant', - }; - - await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData); - - const response = await request(app) - .get('/api/combat/active') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data).toHaveProperty('combats'); - expect(response.body.data).toHaveProperty('count'); - }); - }); - - describe('Fleet Positioning', () => { - it('should update fleet position successfully', async () => { - const positionData = { - position_x: 100, - position_y: 50, - position_z: 0, - formation: 'aggressive', - tactical_settings: { - engagement_range: 'close', - target_priority: 'closest', - }, - }; - - const response = await request(app) - .put(`/api/combat/position/${attackerFleet.id}`) - .set('Authorization', `Bearer ${authToken}`) - .send(positionData) - .expect(200); - - expect(response.body.success).toBe(true); - expect(response.body.data.formation).toBe('aggressive'); - - // Verify in database - const position = await db('fleet_positions') - .where('fleet_id', attackerFleet.id) - .first(); - - expect(position).toBeDefined(); - expect(position.formation).toBe('aggressive'); - expect(position.position_x).toBe(100); - }); - - it('should reject invalid formation types', async () => { - const positionData = { - formation: 'invalid_formation', - }; - - const response = await request(app) - .put(`/api/combat/position/${attackerFleet.id}`) - .set('Authorization', `Bearer ${authToken}`) - .send(positionData) - .expect(400); - - expect(response.body.error).toContain('Validation failed'); - }); - - it('should prevent updating position of fleet not owned by player', async () => { - const positionData = { - formation: 'defensive', - }; - - const response = await request(app) - .put(`/api/combat/position/${defenderFleet.id}`) - .set('Authorization', `Bearer ${authToken}`) - .send(positionData) - .expect(404); - - expect(response.body.error).toContain('Fleet not found or access denied'); - }); - }); - - describe('Combat Types and Configurations', () => { - it('should retrieve available combat types', async () => { - const response = await request(app) - .get('/api/combat/types') - .set('Authorization', `Bearer ${authToken}`) - .expect(200); - - expect(response.body.success).toBe(true); - expect(Array.isArray(response.body.data)).toBe(true); - expect(response.body.data.length).toBeGreaterThan(0); - }); - - it('should use different combat types', async () => { - // Test turn-based combat - await db('combat_configurations').insert({ - config_name: 'test_turn_based', - combat_type: 'turn_based', - config_data: JSON.stringify({ - auto_resolve: true, - preparation_time: 1, - max_rounds: 3, - }), - is_active: true, - }); - - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_fleet_id: defenderFleet.id, - location: 'A3-91-X', - combat_type: 'turn_based', - }; - - const response = await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData) - .expect(201); - - expect(response.body.success).toBe(true); - - // Wait for resolution - await new Promise(resolve => setTimeout(resolve, 3000)); - - const battleId = response.body.data.battleId; - const encounter = await db('combat_encounters') - .where('battle_id', battleId) - .first(); - - // Turn-based combat should have more detailed logs - const combatLog = JSON.parse(encounter.combat_log); - expect(combatLog.length).toBeGreaterThan(2); - }); - }); - - describe('Error Handling and Validation', () => { - it('should handle missing required fields', async () => { - const incompleteData = { - attacker_fleet_id: attackerFleet.id, - // Missing defender and location - }; - - const response = await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(incompleteData) - .expect(400); - - expect(response.body.error).toBeDefined(); - }); - - it('should handle invalid fleet IDs', async () => { - const invalidData = { - attacker_fleet_id: 99999, - defender_fleet_id: 99998, - location: 'A3-91-X', - }; - - const response = await request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(invalidData) - .expect(400); - - expect(response.body.error).toContain('Invalid attacker fleet'); - }); - - it('should enforce rate limiting', async () => { - const combatData = { - attacker_fleet_id: attackerFleet.id, - defender_fleet_id: defenderFleet.id, - location: 'A3-91-X', - }; - - // Make multiple rapid requests - const promises = Array(15).fill().map(() => - request(app) - .post('/api/combat/initiate') - .set('Authorization', `Bearer ${authToken}`) - .send(combatData), - ); - - const results = await Promise.allSettled(promises); - - // Some requests should be rate limited - const rateLimitedResponses = results.filter(result => - result.status === 'fulfilled' && result.value.status === 429, - ); - - expect(rateLimitedResponses.length).toBeGreaterThan(0); - }); - }); -}); diff --git a/src/tests/integration/game-tick.integration.test.js b/src/tests/integration/game-tick.integration.test.js deleted file mode 100644 index e92e3b4..0000000 --- a/src/tests/integration/game-tick.integration.test.js +++ /dev/null @@ -1,388 +0,0 @@ -/** - * Game Tick Integration Tests - * End-to-end tests for the complete game tick system with real database interactions - */ - -const request = require('supertest'); -const app = require('../../app'); -const db = require('../../database/connection'); -const { gameTickService, initializeGameTick } = require('../../services/game-tick.service'); -const redisClient = require('../../utils/redis'); - -describe('Game Tick Integration Tests', () => { - let testPlayer; - let testColony; - let testBuilding; - let authToken; - - beforeAll(async () => { - // Set up test database - await db.migrate.latest(); - await db.seed.run(); - - // Create test player - const [player] = await db('players').insert({ - username: 'ticktest', - email: 'ticktest@example.com', - password_hash: '$2b$10$test.hash', - user_group: 0, - is_active: true, - email_verified: true, - }).returning('*'); - - testPlayer = player; - - // Create test colony - const [colony] = await db('colonies').insert({ - player_id: testPlayer.id, - name: 'Test Colony', - coordinates: 'TEST-01-A', - planet_type_id: 1, // Terran - population: 1000, - }).returning('*'); - - testColony = colony; - - // Create test building under construction - const [building] = await db('colony_buildings').insert({ - colony_id: testColony.id, - building_type_id: 1, // Command Center - level: 1, - is_under_construction: true, - construction_started: new Date(), - construction_completes: new Date(Date.now() + 1000), // Complete in 1 second - }).returning('*'); - - testBuilding = building; - - // Initialize player resources - const resourceTypes = await db('resource_types').select('*'); - for (const resourceType of resourceTypes) { - await db('player_resources').insert({ - player_id: testPlayer.id, - resource_type_id: resourceType.id, - amount: 1000, - }); - } - - // Initialize game tick service - await initializeGameTick(); - }); - - afterAll(async () => { - await gameTickService.stop(); - await db.destroy(); - await redisClient.quit(); - }); - - beforeEach(async () => { - // Clean up any test data modifications - jest.clearAllMocks(); - }); - - describe('Complete Tick Processing Flow', () => { - it('should process a full game tick for a player', async () => { - const initialTick = gameTickService.currentTick; - const correlationId = 'integration-test-tick'; - - // Trigger a manual tick - await gameTickService.processPlayerTick( - initialTick + 1, - testPlayer.id, - correlationId, - ); - - // Verify player was updated - const updatedPlayer = await db('players') - .where('id', testPlayer.id) - .first(); - - expect(updatedPlayer.last_tick_processed).toBe(initialTick + 1); - expect(updatedPlayer.last_tick_processed_at).toBeTruthy(); - }); - - it('should complete building construction during tick', async (done) => { - // Wait for construction to complete (1 second) - setTimeout(async () => { - try { - const correlationId = 'building-completion-test'; - - await gameTickService.processBuildingConstruction( - testPlayer.id, - gameTickService.currentTick + 1, - correlationId, - db, - ); - - // Check that building is no longer under construction - const completedBuilding = await db('colony_buildings') - .where('id', testBuilding.id) - .first(); - - expect(completedBuilding.is_under_construction).toBe(false); - expect(completedBuilding.construction_completes).toBe(null); - - done(); - } catch (error) { - done(error); - } - }, 1500); - }); - - it('should process resource production correctly', async () => { - // Create production buildings - await db('colony_buildings').insert({ - colony_id: testColony.id, - building_type_id: 2, // Salvage Yard - level: 2, - is_under_construction: false, - }); - - await db('colony_buildings').insert({ - colony_id: testColony.id, - building_type_id: 3, // Power Plant - level: 1, - is_under_construction: false, - }); - - const correlationId = 'resource-production-test'; - - // Get initial resources - const initialResources = await db('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', testPlayer.id) - .select('resource_types.name', 'player_resources.amount'); - - const initialScrap = initialResources.find(r => r.name === 'scrap')?.amount || 0; - const initialEnergy = initialResources.find(r => r.name === 'energy')?.amount || 0; - - // Process resource production - const result = await gameTickService.processResourceProduction( - testPlayer.id, - gameTickService.currentTick + 1, - correlationId, - db, - ); - - expect(result.status).toBe('success'); - expect(result.coloniesProcessed).toBe(1); - expect(result.totalResourcesProduced).toBeDefined(); - - // Verify resources were added to player - const finalResources = await db('player_resources') - .join('resource_types', 'player_resources.resource_type_id', 'resource_types.id') - .where('player_resources.player_id', testPlayer.id) - .select('resource_types.name', 'player_resources.amount'); - - const finalScrap = finalResources.find(r => r.name === 'scrap')?.amount || 0; - const finalEnergy = finalResources.find(r => r.name === 'energy')?.amount || 0; - - // Resources should have increased (if production is positive) - if (result.totalResourcesProduced.scrap > 0) { - expect(finalScrap).toBeGreaterThan(initialScrap); - } - if (result.totalResourcesProduced.energy > 0) { - expect(finalEnergy).toBeGreaterThan(initialEnergy); - } - }); - }); - - describe('Database Performance and Concurrency', () => { - it('should handle concurrent player processing', async () => { - // Create additional test players - const players = []; - for (let i = 0; i < 5; i++) { - const [player] = await db('players').insert({ - username: `concurrent-test-${i}`, - email: `concurrent-test-${i}@example.com`, - password_hash: '$2b$10$test.hash', - user_group: 0, - is_active: true, - email_verified: true, - }).returning('*'); - players.push(player); - } - - const tickNumber = gameTickService.currentTick + 1; - const promises = players.map(player => - gameTickService.processPlayerTick( - tickNumber, - player.id, - `concurrent-test-${player.id}`, - ), - ); - - // All players should process successfully - const results = await Promise.allSettled(promises); - const successfulResults = results.filter(r => r.status === 'fulfilled'); - - expect(successfulResults).toHaveLength(players.length); - - // Verify all players were updated - const updatedPlayers = await db('players') - .whereIn('id', players.map(p => p.id)) - .where('last_tick_processed', tickNumber); - - expect(updatedPlayers).toHaveLength(players.length); - - // Clean up - await db('players').whereIn('id', players.map(p => p.id)).del(); - }); - - it('should handle Redis lock contention', async () => { - const tickNumber = gameTickService.currentTick + 1; - const correlationId1 = 'lock-test-1'; - const correlationId2 = 'lock-test-2'; - - // Start two concurrent processing attempts for the same player - const promise1 = gameTickService.processPlayerTick( - tickNumber, - testPlayer.id, - correlationId1, - ); - - const promise2 = gameTickService.processPlayerTick( - tickNumber, - testPlayer.id, - correlationId2, - ); - - const [result1, result2] = await Promise.allSettled([promise1, promise2]); - - // One should succeed, one should skip due to lock - const successes = [result1, result2].filter(r => r.status === 'fulfilled'); - expect(successes.length).toBeGreaterThanOrEqual(1); - }); - }); - - describe('Error Recovery and Resilience', () => { - it('should handle individual player failures gracefully', async () => { - // Create a player with invalid data that will cause processing to fail - const [problematicPlayer] = await db('players').insert({ - username: 'problematic-player', - email: 'problematic@example.com', - password_hash: '$2b$10$test.hash', - user_group: 0, - is_active: true, - email_verified: true, - }).returning('*'); - - // Create colony with invalid planet_type_id - await db('colonies').insert({ - player_id: problematicPlayer.id, - name: 'Problematic Colony', - coordinates: 'PROB-01-A', - planet_type_id: 9999, // Invalid planet type - population: 1000, - }); - - const tickNumber = gameTickService.currentTick + 1; - - // Processing should handle the error gracefully - await expect( - gameTickService.processPlayerTick( - tickNumber, - problematicPlayer.id, - 'error-recovery-test', - ), - ).rejects.toThrow(); // Should throw error but not crash the service - - // Service should still be operational for other players - await expect( - gameTickService.processPlayerTick( - tickNumber, - testPlayer.id, - 'normal-processing-test', - ), - ).resolves.not.toThrow(); - - // Clean up - await db('colonies').where('player_id', problematicPlayer.id).del(); - await db('players').where('id', problematicPlayer.id).del(); - }); - }); - - describe('Performance Metrics and Monitoring', () => { - it('should track performance metrics during processing', async () => { - const initialMetrics = gameTickService.getStatus().metrics; - - await gameTickService.processPlayerTick( - gameTickService.currentTick + 1, - testPlayer.id, - 'performance-test', - ); - - const finalMetrics = gameTickService.getStatus().metrics; - - // Metrics should be updated - expect(finalMetrics.totalPlayersProcessed).toBeGreaterThanOrEqual( - initialMetrics.totalPlayersProcessed, - ); - }); - - it('should log tick processing activities', async () => { - const tickNumber = gameTickService.currentTick + 1; - - await gameTickService.processPlayerTick( - tickNumber, - testPlayer.id, - 'logging-test', - ); - - // Check that appropriate log entries were created - // This would require examining the logger mock calls in a real test - expect(true).toBe(true); // Placeholder for actual logging verification - }); - }); - - describe('Real-time WebSocket Integration', () => { - it('should emit appropriate WebSocket events during processing', async () => { - // This test would require a mock WebSocket service - // For now, we verify the service can process without WebSocket events - - const serviceWithoutWebSocket = require('../../services/game-tick.service').gameTickService; - serviceWithoutWebSocket.gameEventService = null; - - await expect( - serviceWithoutWebSocket.processPlayerTick( - gameTickService.currentTick + 1, - testPlayer.id, - 'websocket-test', - ), - ).resolves.not.toThrow(); - }); - }); - - describe('Configuration Management', () => { - it('should reload configuration when updated', async () => { - const initialConfig = gameTickService.config; - - // Update configuration in database - await db('game_tick_config') - .where('is_active', true) - .update({ - tick_interval_ms: 90000, - user_groups_count: 15, - }); - - // Reload configuration - await gameTickService.loadConfig(); - - const newConfig = gameTickService.config; - - expect(newConfig.tick_interval_ms).toBe(90000); - expect(newConfig.user_groups_count).toBe(15); - expect(newConfig.tick_interval_ms).not.toBe(initialConfig.tick_interval_ms); - - // Restore original configuration - await db('game_tick_config') - .where('is_active', true) - .update({ - tick_interval_ms: initialConfig.tick_interval_ms, - user_groups_count: initialConfig.user_groups_count, - }); - - await gameTickService.loadConfig(); - }); - }); -}); diff --git a/src/tests/performance/game-tick.performance.test.js b/src/tests/performance/game-tick.performance.test.js deleted file mode 100644 index c219fc4..0000000 --- a/src/tests/performance/game-tick.performance.test.js +++ /dev/null @@ -1,417 +0,0 @@ -/** - * Game Tick Performance Benchmark Tests - * Tests the performance characteristics of the game tick system under various loads - */ - -const { performance } = require('perf_hooks'); -const db = require('../../database/connection'); -const { gameTickService, initializeGameTick } = require('../../services/game-tick.service'); - -describe('Game Tick Performance Benchmarks', () => { - const PERFORMANCE_THRESHOLDS = { - SINGLE_PLAYER_PROCESSING_MS: 100, // Single player should process in under 100ms - BATCH_PROCESSING_MS_PER_PLAYER: 50, // Batch processing should be under 50ms per player - USER_GROUP_PROCESSING_MS: 5000, // User group should process in under 5 seconds - MEMORY_LEAK_THRESHOLD_MB: 50, // Memory should not grow by more than 50MB - }; - - const testPlayers = []; - const testColonies = []; - let initialMemoryUsage; - - beforeAll(async () => { - // Set up test database - await db.migrate.latest(); - await db.seed.run(); - - // Initialize game tick service - await initializeGameTick(); - - // Record initial memory usage - initialMemoryUsage = process.memoryUsage(); - - console.log('Creating test data for performance benchmarks...'); - - // Create test players with varying complexity - for (let i = 0; i < 100; i++) { - const [player] = await db('players').insert({ - username: `perf-test-${i}`, - email: `perf-test-${i}@example.com`, - password_hash: '$2b$10$test.hash', - user_group: i % 10, // Distribute across 10 user groups - is_active: true, - email_verified: true, - }).returning('*'); - - testPlayers.push(player); - - // Create 1-5 colonies per player - const colonyCount = Math.floor(Math.random() * 5) + 1; - for (let j = 0; j < colonyCount; j++) { - const [colony] = await db('colonies').insert({ - player_id: player.id, - name: `Colony ${j + 1}`, - coordinates: `PERF-${i.toString().padStart(2, '0')}-${String.fromCharCode(65 + j)}`, - planet_type_id: (j % 6) + 1, // Cycle through planet types - population: Math.floor(Math.random() * 5000) + 1000, - }).returning('*'); - - testColonies.push(colony); - - // Add buildings to colonies - const buildingCount = Math.floor(Math.random() * 8) + 2; - for (let k = 0; k < buildingCount; k++) { - await db('colony_buildings').insert({ - colony_id: colony.id, - building_type_id: (k % 8) + 1, - level: Math.floor(Math.random() * 5) + 1, - is_under_construction: false, - }); - } - - // Initialize colony resource production - const resourceTypes = await db('resource_types').select('*'); - for (const resourceType of resourceTypes) { - await db('colony_resource_production').insert({ - colony_id: colony.id, - resource_type_id: resourceType.id, - production_rate: Math.floor(Math.random() * 50) + 10, - consumption_rate: Math.floor(Math.random() * 20), - current_stored: Math.floor(Math.random() * 1000) + 100, - storage_capacity: 5000, - }); - } - } - - // Initialize player resources - const resourceTypes = await db('resource_types').select('*'); - for (const resourceType of resourceTypes) { - await db('player_resources').insert({ - player_id: player.id, - resource_type_id: resourceType.id, - amount: Math.floor(Math.random() * 10000) + 1000, - }); - } - } - - console.log(`Created ${testPlayers.length} players with ${testColonies.length} colonies`); - }); - - afterAll(async () => { - // Clean up test data - await db('colony_resource_production').whereIn('colony_id', testColonies.map(c => c.id)).del(); - await db('colony_buildings').whereIn('colony_id', testColonies.map(c => c.id)).del(); - await db('colonies').whereIn('id', testColonies.map(c => c.id)).del(); - await db('player_resources').whereIn('player_id', testPlayers.map(p => p.id)).del(); - await db('players').whereIn('id', testPlayers.map(p => p.id)).del(); - - await gameTickService.stop(); - await db.destroy(); - }); - - describe('Single Player Processing Performance', () => { - it('should process a single player within performance threshold', async () => { - const player = testPlayers[0]; - const tickNumber = 1; - const correlationId = 'single-player-perf-test'; - - const startTime = performance.now(); - - await gameTickService.processPlayerTick(tickNumber, player.id, correlationId); - - const endTime = performance.now(); - const duration = endTime - startTime; - - console.log(`Single player processing took ${duration.toFixed(2)}ms`); - - expect(duration).toBeLessThan(PERFORMANCE_THRESHOLDS.SINGLE_PLAYER_PROCESSING_MS); - }); - - it('should process complex player with many colonies efficiently', async () => { - // Find player with most colonies - const playerColonyCounts = await db('colonies') - .select('player_id') - .count('* as colony_count') - .whereIn('player_id', testPlayers.map(p => p.id)) - .groupBy('player_id') - .orderBy('colony_count', 'desc') - .first(); - - const complexPlayer = testPlayers.find(p => p.id === playerColonyCounts.player_id); - const tickNumber = 2; - const correlationId = 'complex-player-perf-test'; - - const startTime = performance.now(); - - await gameTickService.processPlayerTick(tickNumber, complexPlayer.id, correlationId); - - const endTime = performance.now(); - const duration = endTime - startTime; - - console.log(`Complex player (${playerColonyCounts.colony_count} colonies) processing took ${duration.toFixed(2)}ms`); - - // More lenient threshold for complex players - expect(duration).toBeLessThan(PERFORMANCE_THRESHOLDS.SINGLE_PLAYER_PROCESSING_MS * 2); - }); - }); - - describe('Batch Processing Performance', () => { - it('should process multiple players efficiently in parallel', async () => { - const batchSize = 10; - const playerBatch = testPlayers.slice(0, batchSize); - const tickNumber = 3; - - const startTime = performance.now(); - - const promises = playerBatch.map(player => - gameTickService.processPlayerTick(tickNumber, player.id, `batch-perf-test-${player.id}`), - ); - - await Promise.allSettled(promises); - - const endTime = performance.now(); - const duration = endTime - startTime; - const durationPerPlayer = duration / batchSize; - - console.log(`Batch processing ${batchSize} players took ${duration.toFixed(2)}ms (${durationPerPlayer.toFixed(2)}ms per player)`); - - expect(durationPerPlayer).toBeLessThan(PERFORMANCE_THRESHOLDS.BATCH_PROCESSING_MS_PER_PLAYER); - }); - - it('should maintain performance with larger batches', async () => { - const batchSize = 25; - const playerBatch = testPlayers.slice(10, 10 + batchSize); - const tickNumber = 4; - - const startTime = performance.now(); - - const promises = playerBatch.map(player => - gameTickService.processPlayerTick(tickNumber, player.id, `large-batch-perf-test-${player.id}`), - ); - - await Promise.allSettled(promises); - - const endTime = performance.now(); - const duration = endTime - startTime; - const durationPerPlayer = duration / batchSize; - - console.log(`Large batch processing ${batchSize} players took ${duration.toFixed(2)}ms (${durationPerPlayer.toFixed(2)}ms per player)`); - - // Slightly more lenient for larger batches due to concurrency overhead - expect(durationPerPlayer).toBeLessThan(PERFORMANCE_THRESHOLDS.BATCH_PROCESSING_MS_PER_PLAYER * 1.5); - }); - }); - - describe('User Group Processing Performance', () => { - it('should process an entire user group within threshold', async () => { - const userGroup = 0; - const tickNumber = 5; - - // Get players in this user group - const userGroupPlayers = testPlayers.filter(p => p.user_group === userGroup); - - console.log(`Processing user group ${userGroup} with ${userGroupPlayers.length} players`); - - const startTime = performance.now(); - - await gameTickService.processUserGroupTick(tickNumber, userGroup); - - const endTime = performance.now(); - const duration = endTime - startTime; - - console.log(`User group ${userGroup} processing took ${duration.toFixed(2)}ms`); - - expect(duration).toBeLessThan(PERFORMANCE_THRESHOLDS.USER_GROUP_PROCESSING_MS); - - // Verify all players in the group were processed - const processedPlayers = await db('players') - .where('user_group', userGroup) - .where('last_tick_processed', tickNumber); - - expect(processedPlayers).toHaveLength(userGroupPlayers.length); - }); - }); - - describe('Resource Production Performance', () => { - it('should calculate resource production efficiently', async () => { - const player = testPlayers[0]; - const tickNumber = 6; - const correlationId = 'resource-production-perf-test'; - - const startTime = performance.now(); - - const result = await gameTickService.processResourceProduction( - player.id, tickNumber, correlationId, db, - ); - - const endTime = performance.now(); - const duration = endTime - startTime; - - console.log(`Resource production processing took ${duration.toFixed(2)}ms`); - - expect(result.status).toBe('success'); - expect(duration).toBeLessThan(50); // Should be very fast - }); - - it('should handle resource production for player with many colonies', async () => { - // Find player with most colonies - const playerColonyCounts = await db('colonies') - .select('player_id') - .count('* as colony_count') - .whereIn('player_id', testPlayers.map(p => p.id)) - .groupBy('player_id') - .orderBy('colony_count', 'desc') - .first(); - - const complexPlayer = testPlayers.find(p => p.id === playerColonyCounts.player_id); - const tickNumber = 7; - const correlationId = 'complex-resource-production-perf-test'; - - const startTime = performance.now(); - - const result = await gameTickService.processResourceProduction( - complexPlayer.id, tickNumber, correlationId, db, - ); - - const endTime = performance.now(); - const duration = endTime - startTime; - - console.log(`Complex resource production (${playerColonyCounts.colony_count} colonies) took ${duration.toFixed(2)}ms`); - - expect(result.status).toBe('success'); - expect(duration).toBeLessThan(100); // Still should be fast - }); - }); - - describe('Database Performance', () => { - it('should handle concurrent database operations efficiently', async () => { - const batchSize = 20; - const playerBatch = testPlayers.slice(20, 20 + batchSize); - const tickNumber = 8; - - // Monitor database connection pool - const initialConnections = db.client.pool.numUsed(); - - const startTime = performance.now(); - - const promises = playerBatch.map(player => - gameTickService.processPlayerTick(tickNumber, player.id, `db-perf-test-${player.id}`), - ); - - await Promise.allSettled(promises); - - const endTime = performance.now(); - const duration = endTime - startTime; - - const finalConnections = db.client.pool.numUsed(); - - console.log(`Database concurrent operations took ${duration.toFixed(2)}ms`); - console.log(`DB connections: ${initialConnections} -> ${finalConnections}`); - - // Should not exhaust connection pool - expect(finalConnections).toBeLessThanOrEqual(db.client.pool.max || 10); - }); - }); - - describe('Memory Usage', () => { - it('should not have significant memory leaks during processing', async () => { - const batchSize = 30; - const playerBatch = testPlayers.slice(30, 30 + batchSize); - const tickNumber = 9; - - // Force garbage collection if available - if (global.gc) { - global.gc(); - } - - const startMemory = process.memoryUsage(); - - // Process multiple batches to stress test memory usage - for (let batch = 0; batch < 3; batch++) { - const promises = playerBatch.map(player => - gameTickService.processPlayerTick(tickNumber + batch, player.id, `memory-test-${batch}-${player.id}`), - ); - - await Promise.allSettled(promises); - } - - // Force garbage collection again - if (global.gc) { - global.gc(); - } - - const endMemory = process.memoryUsage(); - const memoryGrowthMB = (endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024; - - console.log(`Memory growth after processing: ${memoryGrowthMB.toFixed(2)}MB`); - console.log(`Heap used: ${(endMemory.heapUsed / 1024 / 1024).toFixed(2)}MB`); - - expect(memoryGrowthMB).toBeLessThan(PERFORMANCE_THRESHOLDS.MEMORY_LEAK_THRESHOLD_MB); - }); - }); - - describe('Service Metrics and Monitoring', () => { - it('should maintain accurate performance metrics', async () => { - const initialMetrics = gameTickService.getStatus().metrics; - const batchSize = 15; - const playerBatch = testPlayers.slice(60, 60 + batchSize); - const tickNumber = 10; - - const promises = playerBatch.map(player => - gameTickService.processPlayerTick(tickNumber, player.id, `metrics-test-${player.id}`), - ); - - await Promise.allSettled(promises); - - const finalMetrics = gameTickService.getStatus().metrics; - - expect(finalMetrics.totalPlayersProcessed).toBeGreaterThan(initialMetrics.totalPlayersProcessed); - expect(finalMetrics.averageTickDuration).toBeGreaterThan(0); - }); - - it('should provide comprehensive status information quickly', async () => { - const startTime = performance.now(); - - const status = gameTickService.getStatus(); - - const endTime = performance.now(); - const duration = endTime - startTime; - - console.log(`Status retrieval took ${duration.toFixed(2)}ms`); - - expect(duration).toBeLessThan(10); // Status should be instant - expect(status).toHaveProperty('initialized'); - expect(status).toHaveProperty('metrics'); - expect(status).toHaveProperty('config'); - }); - }); - - describe('Scalability Tests', () => { - it('should handle processing all test players in reasonable time', async () => { - const tickNumber = 11; - - console.log(`Processing all ${testPlayers.length} players...`); - - const startTime = performance.now(); - - // Process in user groups as the system would do - const userGroupPromises = []; - for (let userGroup = 0; userGroup < 10; userGroup++) { - userGroupPromises.push( - gameTickService.processUserGroupTick(tickNumber, userGroup), - ); - } - - await Promise.allSettled(userGroupPromises); - - const endTime = performance.now(); - const duration = endTime - startTime; - const durationPerPlayer = duration / testPlayers.length; - - console.log(`All ${testPlayers.length} players processed in ${duration.toFixed(2)}ms (${durationPerPlayer.toFixed(2)}ms per player)`); - - // Should scale reasonably - expect(durationPerPlayer).toBeLessThan(PERFORMANCE_THRESHOLDS.BATCH_PROCESSING_MS_PER_PLAYER * 2); - }, 30000); // 30 second timeout for this test - }); -}); diff --git a/src/tests/unit/services/combat/CombatPluginManager.test.js b/src/tests/unit/services/combat/CombatPluginManager.test.js deleted file mode 100644 index 9786bc9..0000000 --- a/src/tests/unit/services/combat/CombatPluginManager.test.js +++ /dev/null @@ -1,530 +0,0 @@ -/** - * Combat Plugin Manager Unit Tests - * Tests for combat plugin system and resolution strategies - */ - -const { - CombatPluginManager, - InstantCombatPlugin, - TurnBasedCombatPlugin, - TacticalCombatPlugin, -} = require('../../../../services/combat/CombatPluginManager'); -const db = require('../../../../database/connection'); -const logger = require('../../../../utils/logger'); - -// Mock dependencies -jest.mock('../../../../database/connection'); -jest.mock('../../../../utils/logger'); - -describe('CombatPluginManager', () => { - let pluginManager; - - beforeEach(() => { - jest.clearAllMocks(); - pluginManager = new CombatPluginManager(); - - // Mock logger - logger.info = jest.fn(); - logger.error = jest.fn(); - logger.warn = jest.fn(); - logger.debug = jest.fn(); - }); - - describe('initialize', () => { - it('should initialize and load active plugins', async () => { - const mockPlugins = [ - { - id: 1, - name: 'instant_combat', - version: '1.0.0', - plugin_type: 'combat', - is_active: true, - config: { damage_variance: 0.1 }, - hooks: ['pre_combat', 'post_combat'], - }, - { - id: 2, - name: 'turn_based_combat', - version: '1.0.0', - plugin_type: 'combat', - is_active: true, - config: { max_rounds: 10 }, - hooks: ['pre_combat', 'post_combat'], - }, - ]; - - const mockQuery = { - where: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - }; - db.mockReturnValue(mockQuery); - mockQuery.mockResolvedValue(mockPlugins); - - await pluginManager.initialize('test-correlation'); - - expect(pluginManager.initialized).toBe(true); - expect(pluginManager.plugins.size).toBe(2); - expect(pluginManager.plugins.has('instant_combat')).toBe(true); - expect(pluginManager.plugins.has('turn_based_combat')).toBe(true); - }); - - it('should handle initialization errors gracefully', async () => { - const error = new Error('Database connection failed'); - db.mockImplementation(() => { - throw error; - }); - - await expect(pluginManager.initialize('test-correlation')) - .rejects - .toThrow('Failed to initialize combat plugin system'); - - expect(logger.error).toHaveBeenCalledWith( - 'Failed to initialize Combat Plugin Manager', - expect.objectContaining({ - error: error.message, - }), - ); - }); - }); - - describe('resolveCombat', () => { - const mockBattle = { - id: 100, - battle_type: 'fleet_vs_fleet', - location: 'A3-91-X', - }; - - const mockForces = { - attacker: { - fleet: { - id: 1, - total_combat_rating: 150, - ships: [{ design_name: 'Fighter', quantity: 10 }], - }, - }, - defender: { - fleet: { - id: 2, - total_combat_rating: 120, - ships: [{ design_name: 'Cruiser', quantity: 5 }], - }, - }, - }; - - const mockConfig = { - id: 1, - combat_type: 'instant', - config_data: { auto_resolve: true }, - }; - - beforeEach(() => { - pluginManager.initialized = true; - }); - - it('should resolve combat using appropriate plugin', async () => { - const mockPlugin = { - resolveCombat: jest.fn().mockResolvedValue({ - outcome: 'attacker_victory', - casualties: { attacker: { ships: {}, total_ships: 2 }, defender: { ships: {}, total_ships: 8 } }, - experience_gained: 100, - combat_log: [], - duration: 60, - }), - }; - - pluginManager.plugins.set('instant_combat', mockPlugin); - pluginManager.executeHooks = jest.fn(); - - const result = await pluginManager.resolveCombat(mockBattle, mockForces, mockConfig, 'test-correlation'); - - expect(result.outcome).toBe('attacker_victory'); - expect(mockPlugin.resolveCombat).toHaveBeenCalledWith(mockBattle, mockForces, mockConfig, 'test-correlation'); - expect(pluginManager.executeHooks).toHaveBeenCalledWith('pre_combat', expect.any(Object), 'test-correlation'); - expect(pluginManager.executeHooks).toHaveBeenCalledWith('post_combat', expect.any(Object), 'test-correlation'); - }); - - it('should use fallback resolver when plugin not found', async () => { - pluginManager.fallbackCombatResolver = jest.fn().mockResolvedValue({ - outcome: 'attacker_victory', - casualties: {}, - experience_gained: 50, - duration: 30, - }); - - const result = await pluginManager.resolveCombat(mockBattle, mockForces, mockConfig, 'test-correlation'); - - expect(result.outcome).toBe('attacker_victory'); - expect(pluginManager.fallbackCombatResolver).toHaveBeenCalled(); - expect(logger.warn).toHaveBeenCalledWith( - 'No plugin found for combat type, using fallback', - expect.objectContaining({ - combatType: 'instant', - }), - ); - }); - - it('should initialize if not already initialized', async () => { - pluginManager.initialized = false; - pluginManager.initialize = jest.fn(); - pluginManager.fallbackCombatResolver = jest.fn().mockResolvedValue({ - outcome: 'draw', - casualties: {}, - experience_gained: 0, - duration: 15, - }); - - await pluginManager.resolveCombat(mockBattle, mockForces, mockConfig, 'test-correlation'); - - expect(pluginManager.initialize).toHaveBeenCalledWith('test-correlation'); - }); - }); - - describe('executeHooks', () => { - it('should execute all registered hooks for an event', async () => { - const mockHandler1 = jest.fn(); - const mockHandler2 = jest.fn(); - - pluginManager.hooks.set('pre_combat', [ - { plugin: 'plugin1', handler: mockHandler1 }, - { plugin: 'plugin2', handler: mockHandler2 }, - ]); - - const hookData = { battle: {}, forces: {} }; - await pluginManager.executeHooks('pre_combat', hookData, 'test-correlation'); - - expect(mockHandler1).toHaveBeenCalledWith(hookData, 'test-correlation'); - expect(mockHandler2).toHaveBeenCalledWith(hookData, 'test-correlation'); - }); - - it('should handle hook execution errors gracefully', async () => { - const errorHandler = jest.fn().mockRejectedValue(new Error('Hook failed')); - const successHandler = jest.fn(); - - pluginManager.hooks.set('post_combat', [ - { plugin: 'failing_plugin', handler: errorHandler }, - { plugin: 'working_plugin', handler: successHandler }, - ]); - - const hookData = { result: {} }; - await pluginManager.executeHooks('post_combat', hookData, 'test-correlation'); - - expect(errorHandler).toHaveBeenCalled(); - expect(successHandler).toHaveBeenCalled(); - expect(logger.error).toHaveBeenCalledWith( - 'Hook execution failed', - expect.objectContaining({ - hookName: 'post_combat', - plugin: 'failing_plugin', - }), - ); - }); - }); - - describe('registerPlugin', () => { - it('should register plugin dynamically', () => { - const mockPlugin = { - resolveCombat: jest.fn(), - }; - - pluginManager.registerPlugin('test_plugin', mockPlugin, ['pre_combat']); - - expect(pluginManager.plugins.has('test_plugin')).toBe(true); - expect(pluginManager.hooks.has('pre_combat')).toBe(true); - expect(pluginManager.hooks.get('pre_combat')).toHaveLength(1); - }); - - it('should validate plugin interface before registration', () => { - const invalidPlugin = { - // Missing required resolveCombat method - someOtherMethod: jest.fn(), - }; - - expect(() => { - pluginManager.registerPlugin('invalid_plugin', invalidPlugin); - }).toThrow('Plugin invalid_plugin missing required method: resolveCombat'); - }); - }); -}); - -describe('InstantCombatPlugin', () => { - let plugin; - - beforeEach(() => { - plugin = new InstantCombatPlugin({ damage_variance: 0.1, experience_gain: 1.0 }); - }); - - describe('resolveCombat', () => { - const mockBattle = { - id: 100, - battle_type: 'fleet_vs_fleet', - }; - - const mockForces = { - attacker: { - fleet: { - total_combat_rating: 200, - ships: [ - { design_name: 'Fighter', quantity: 20 }, - { design_name: 'Destroyer', quantity: 5 }, - ], - }, - }, - defender: { - fleet: { - total_combat_rating: 150, - ships: [ - { design_name: 'Cruiser', quantity: 8 }, - ], - }, - }, - }; - - const mockConfig = {}; - - it('should resolve instant combat with attacker advantage', async () => { - // Mock Math.random to ensure consistent results - const originalRandom = Math.random; - Math.random = jest.fn().mockReturnValue(0.3); // Favors attacker - - const result = await plugin.resolveCombat(mockBattle, mockForces, mockConfig, 'test-correlation'); - - expect(result.outcome).toBe('attacker_victory'); - expect(result.casualties.attacker.total_ships).toBeLessThan(result.casualties.defender.total_ships); - expect(result.experience_gained).toBeGreaterThan(0); - expect(result.combat_log).toHaveLength(2); - expect(result.duration).toBeGreaterThan(0); - - Math.random = originalRandom; - }); - - it('should resolve instant combat with defender advantage', async () => { - const originalRandom = Math.random; - Math.random = jest.fn().mockReturnValue(0.8); // Favors defender - - const result = await plugin.resolveCombat(mockBattle, mockForces, mockConfig, 'test-correlation'); - - expect(result.outcome).toBe('defender_victory'); - expect(result.casualties.defender.total_ships).toBeLessThan(result.casualties.attacker.total_ships); - - Math.random = originalRandom; - }); - - it('should handle colony defense scenario', async () => { - const colonyForces = { - ...mockForces, - defender: { - colony: { - total_defense_rating: 180, - defense_buildings: [ - { building_name: 'Defense Grid', health_percentage: 100 }, - ], - }, - }, - }; - - const originalRandom = Math.random; - Math.random = jest.fn().mockReturnValue(0.2); - - const result = await plugin.resolveCombat(mockBattle, colonyForces, mockConfig, 'test-correlation'); - - expect(result.outcome).toBe('attacker_victory'); - expect(result.casualties.defender.buildings).toBeDefined(); - expect(result.loot.data_cores).toBeGreaterThan(0); - - Math.random = originalRandom; - }); - }); - - describe('calculateInstantCasualties', () => { - it('should calculate casualties with winner advantage', () => { - const forces = { - attacker: { - fleet: { - ships: [ - { design_name: 'Fighter', quantity: 50 }, - ], - }, - }, - defender: { - fleet: { - ships: [ - { design_name: 'Cruiser', quantity: 20 }, - ], - }, - }, - }; - - const casualties = plugin.calculateInstantCasualties(forces, true); - - expect(casualties.attacker.total_ships).toBeLessThan(casualties.defender.total_ships); - expect(casualties.attacker.ships['Fighter']).toBeLessThan(15); // Winner loses max 25% - expect(casualties.defender.ships['Cruiser']).toBeGreaterThan(5); // Loser loses min 30% - }); - }); -}); - -describe('TurnBasedCombatPlugin', () => { - let plugin; - - beforeEach(() => { - plugin = new TurnBasedCombatPlugin({ max_rounds: 5 }); - }); - - describe('resolveCombat', () => { - const mockBattle = { - id: 100, - battle_type: 'fleet_vs_fleet', - }; - - const mockForces = { - attacker: { - fleet: { - total_combat_rating: 300, - ships: [{ design_name: 'Battleship', quantity: 10 }], - }, - }, - defender: { - fleet: { - total_combat_rating: 200, - ships: [{ design_name: 'Destroyer', quantity: 15 }], - }, - }, - }; - - const mockConfig = {}; - - it('should resolve turn-based combat over multiple rounds', async () => { - const result = await plugin.resolveCombat(mockBattle, mockForces, mockConfig, 'test-correlation'); - - expect(result.combat_log.length).toBeGreaterThan(2); // Start + multiple rounds + end - expect(result.combat_log[0].event).toBe('combat_start'); - expect(result.combat_log[result.combat_log.length - 1].event).toBe('combat_end'); - expect(result.duration).toBeGreaterThan(30); // Multiple rounds take longer - }); - - it('should end combat when one side is eliminated', async () => { - // Mock very weak defender - const weakForces = { - ...mockForces, - defender: { - fleet: { - total_combat_rating: 10, - ships: [{ design_name: 'Fighter', quantity: 1 }], - }, - }, - }; - - const result = await plugin.resolveCombat(mockBattle, weakForces, mockConfig, 'test-correlation'); - - expect(result.outcome).toBe('attacker_victory'); - const endLog = result.combat_log.find(log => log.event === 'combat_end'); - expect(endLog.data.defender_survivors).toBe(0); - }); - }); - - describe('initializeCombatState', () => { - it('should properly initialize combat state from forces', () => { - const forces = { - attacker: { - fleet: { - ships: [ - { quantity: 10 }, - { quantity: 5 }, - ], - total_combat_rating: 150, - }, - }, - defender: { - colony: { - total_defense_rating: 100, - }, - }, - }; - - const state = plugin.initializeCombatState(forces); - - expect(state.attacker.totalShips).toBe(15); - expect(state.attacker.effectiveStrength).toBe(150); - expect(state.defender.totalShips).toBe(1); // Colony represented as single entity - expect(state.defender.effectiveStrength).toBe(100); - }); - }); - - describe('determineTurnBasedOutcome', () => { - it('should determine attacker victory', () => { - const state = { - attacker: { totalShips: 5 }, - defender: { totalShips: 0 }, - }; - - const outcome = plugin.determineTurnBasedOutcome(state); - expect(outcome).toBe('attacker_victory'); - }); - - it('should determine defender victory', () => { - const state = { - attacker: { totalShips: 0 }, - defender: { totalShips: 3 }, - }; - - const outcome = plugin.determineTurnBasedOutcome(state); - expect(outcome).toBe('defender_victory'); - }); - - it('should determine draw', () => { - const state = { - attacker: { totalShips: 0 }, - defender: { totalShips: 0 }, - }; - - const outcome = plugin.determineTurnBasedOutcome(state); - expect(outcome).toBe('draw'); - }); - }); -}); - -describe('TacticalCombatPlugin', () => { - let plugin; - - beforeEach(() => { - plugin = new TacticalCombatPlugin({}); - }); - - describe('resolveCombat', () => { - it('should provide enhanced rewards compared to turn-based combat', async () => { - const mockBattle = { - id: 100, - battle_type: 'fleet_vs_fleet', - }; - - const mockForces = { - attacker: { - fleet: { - total_combat_rating: 200, - ships: [{ design_name: 'Cruiser', quantity: 10 }], - }, - }, - defender: { - fleet: { - total_combat_rating: 150, - ships: [{ design_name: 'Destroyer', quantity: 12 }], - }, - }, - }; - - const mockConfig = {}; - - const result = await plugin.resolveCombat(mockBattle, mockForces, mockConfig, 'test-correlation'); - - // Tactical combat should give 1.5x experience - expect(result.experience_gained).toBeGreaterThan(0); - // Duration should be 1.2x longer - expect(result.duration).toBeGreaterThan(30); - - // Loot should be enhanced by 1.3x - if (result.loot && Object.keys(result.loot).length > 0) { - expect(result.loot.scrap).toBeGreaterThan(0); - } - }); - }); -}); diff --git a/src/tests/unit/services/combat/CombatService.test.js b/src/tests/unit/services/combat/CombatService.test.js deleted file mode 100644 index fc1900e..0000000 --- a/src/tests/unit/services/combat/CombatService.test.js +++ /dev/null @@ -1,603 +0,0 @@ -/** - * Combat Service Unit Tests - * Tests for core combat service functionality - */ - -const CombatService = require('../../../../services/combat/CombatService'); -const { CombatPluginManager } = require('../../../../services/combat/CombatPluginManager'); -const db = require('../../../../database/connection'); -const logger = require('../../../../utils/logger'); - -// Mock dependencies -jest.mock('../../../../database/connection'); -jest.mock('../../../../utils/logger'); -jest.mock('../../../../services/combat/CombatPluginManager'); - -describe('CombatService', () => { - let combatService; - let mockGameEventService; - let mockCombatPluginManager; - - beforeEach(() => { - // Reset all mocks - jest.clearAllMocks(); - - // Mock game event service - mockGameEventService = { - emitCombatInitiated: jest.fn(), - emitCombatCompleted: jest.fn(), - emitCombatStatusUpdate: jest.fn(), - }; - - // Mock combat plugin manager - mockCombatPluginManager = { - resolveCombat: jest.fn(), - initialize: jest.fn(), - }; - - // Create service instance - combatService = new CombatService(mockGameEventService, mockCombatPluginManager); - - // Mock logger - logger.info = jest.fn(); - logger.error = jest.fn(); - logger.warn = jest.fn(); - logger.debug = jest.fn(); - }); - - describe('initiateCombat', () => { - const mockCombatData = { - attacker_fleet_id: 1, - defender_fleet_id: 2, - location: 'A3-91-X', - combat_type: 'instant', - }; - - const mockAttackerPlayerId = 10; - const correlationId = 'test-correlation-id'; - - beforeEach(() => { - // Mock database operations - db.transaction = jest.fn(); - - // Mock validation methods - combatService.validateCombatInitiation = jest.fn(); - combatService.checkCombatConflicts = jest.fn().mockResolvedValue({ hasConflict: false }); - combatService.getCombatConfiguration = jest.fn().mockResolvedValue({ - id: 1, - combat_type: 'instant', - config_data: { auto_resolve: true, preparation_time: 30 }, - }); - }); - - it('should successfully initiate combat between fleets', async () => { - const mockBattle = { - id: 100, - participants: JSON.stringify({ - attacker_fleet_id: 1, - defender_fleet_id: 2, - attacker_player_id: 10, - }), - started_at: new Date(), - estimated_duration: 60, - }; - - // Mock transaction success - db.transaction.mockImplementation(async (callback) => { - const mockTrx = { - battles: { - insert: jest.fn().mockReturnValue({ - returning: jest.fn().mockResolvedValue([mockBattle]), - }), - }, - fleets: { - whereIn: jest.fn().mockReturnValue({ - update: jest.fn().mockResolvedValue(), - }), - }, - combat_queue: { - insert: jest.fn().mockResolvedValue(), - }, - }; - return callback(mockTrx); - }); - - const result = await combatService.initiateCombat(mockCombatData, mockAttackerPlayerId, correlationId); - - expect(result).toHaveProperty('battleId', 100); - expect(result).toHaveProperty('status'); - expect(combatService.validateCombatInitiation).toHaveBeenCalledWith( - mockCombatData, - mockAttackerPlayerId, - correlationId, - ); - expect(mockGameEventService.emitCombatInitiated).toHaveBeenCalledWith(mockBattle, correlationId); - }); - - it('should reject combat if participant already in combat', async () => { - combatService.checkCombatConflicts.mockResolvedValue({ - hasConflict: true, - reason: 'Fleet 1 is already in combat', - }); - - await expect(combatService.initiateCombat(mockCombatData, mockAttackerPlayerId, correlationId)) - .rejects - .toThrow('Combat participant already engaged: Fleet 1 is already in combat'); - }); - - it('should handle validation errors', async () => { - const validationError = new Error('Invalid combat data'); - combatService.validateCombatInitiation.mockRejectedValue(validationError); - - await expect(combatService.initiateCombat(mockCombatData, mockAttackerPlayerId, correlationId)) - .rejects - .toThrow(); - - expect(logger.error).toHaveBeenCalledWith( - 'Combat initiation failed', - expect.objectContaining({ - correlationId, - playerId: mockAttackerPlayerId, - error: validationError.message, - }), - ); - }); - - it('should handle database transaction failures', async () => { - const dbError = new Error('Database connection failed'); - db.transaction.mockRejectedValue(dbError); - - await expect(combatService.initiateCombat(mockCombatData, mockAttackerPlayerId, correlationId)) - .rejects - .toThrow(); - - expect(logger.error).toHaveBeenCalled(); - }); - }); - - describe('processCombat', () => { - const battleId = 100; - const correlationId = 'test-correlation-id'; - - beforeEach(() => { - combatService.getBattleById = jest.fn(); - combatService.getCombatForces = jest.fn(); - combatService.getCombatConfiguration = jest.fn(); - combatService.resolveCombat = jest.fn(); - combatService.applyCombatResults = jest.fn(); - combatService.updateCombatStatistics = jest.fn(); - - db.transaction = jest.fn(); - }); - - it('should successfully process combat', async () => { - const mockBattle = { - id: battleId, - status: 'preparing', - participants: JSON.stringify({ attacker_fleet_id: 1, defender_fleet_id: 2 }), - started_at: new Date(), - }; - - const mockForces = { - attacker: { fleet: { id: 1, total_combat_rating: 100 } }, - defender: { fleet: { id: 2, total_combat_rating: 80 } }, - initial: {}, - }; - - const mockCombatResult = { - outcome: 'attacker_victory', - casualties: { attacker: { ships: {}, total_ships: 5 }, defender: { ships: {}, total_ships: 12 } }, - experience_gained: 150, - combat_log: [], - duration: 90, - final_forces: {}, - loot: { scrap: 500, energy: 300 }, - }; - - combatService.getBattleById.mockResolvedValue(mockBattle); - combatService.getCombatForces.mockResolvedValue(mockForces); - combatService.getCombatConfiguration.mockResolvedValue({ - id: 1, - combat_type: 'instant', - }); - combatService.resolveCombat.mockResolvedValue(mockCombatResult); - - // Mock transaction - db.transaction.mockImplementation(async (callback) => { - const mockTrx = { - battles: { - where: jest.fn().mockReturnValue({ - update: jest.fn().mockResolvedValue(), - }), - }, - combat_encounters: { - insert: jest.fn().mockReturnValue({ - returning: jest.fn().mockResolvedValue([{ - id: 200, - battle_id: battleId, - outcome: 'attacker_victory', - }]), - }), - }, - combat_queue: { - where: jest.fn().mockReturnValue({ - update: jest.fn().mockResolvedValue(), - }), - }, - }; - return callback(mockTrx); - }); - - const result = await combatService.processCombat(battleId, correlationId); - - expect(result).toHaveProperty('battleId', battleId); - expect(result).toHaveProperty('outcome', 'attacker_victory'); - expect(combatService.applyCombatResults).toHaveBeenCalled(); - expect(combatService.updateCombatStatistics).toHaveBeenCalled(); - expect(mockGameEventService.emitCombatCompleted).toHaveBeenCalled(); - }); - - it('should reject processing non-existent battle', async () => { - combatService.getBattleById.mockResolvedValue(null); - - await expect(combatService.processCombat(battleId, correlationId)) - .rejects - .toThrow('Battle not found'); - }); - - it('should reject processing completed battle', async () => { - const completedBattle = { - id: battleId, - status: 'completed', - participants: JSON.stringify({ attacker_fleet_id: 1 }), - }; - - combatService.getBattleById.mockResolvedValue(completedBattle); - - await expect(combatService.processCombat(battleId, correlationId)) - .rejects - .toThrow('Battle is not in a processable state'); - }); - }); - - describe('getCombatHistory', () => { - const playerId = 10; - const correlationId = 'test-correlation-id'; - - beforeEach(() => { - // Mock database query builder - const mockQuery = { - select: jest.fn().mockReturnThis(), - join: jest.fn().mockReturnThis(), - leftJoin: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - offset: jest.fn().mockReturnThis(), - }; - - db.mockReturnValue(mockQuery); - }); - - it('should return combat history with pagination', async () => { - const mockCombats = [ - { - id: 1, - outcome: 'attacker_victory', - completed_at: new Date(), - attacker_fleet_name: 'Test Fleet 1', - }, - { - id: 2, - outcome: 'defender_victory', - completed_at: new Date(), - defender_colony_name: 'Test Colony', - }, - ]; - - const mockCountResult = [{ total: 25 }]; - - // Mock the main query - db().mockResolvedValueOnce(mockCombats); - // Mock the count query - db().mockResolvedValueOnce(mockCountResult); - - const options = { limit: 10, offset: 0 }; - const result = await combatService.getCombatHistory(playerId, options, correlationId); - - expect(result).toHaveProperty('combats'); - expect(result).toHaveProperty('pagination'); - expect(result.combats).toHaveLength(2); - expect(result.pagination.total).toBe(25); - expect(result.pagination.hasMore).toBe(true); - }); - - it('should filter by outcome when specified', async () => { - const mockQuery = { - select: jest.fn().mockReturnThis(), - join: jest.fn().mockReturnThis(), - leftJoin: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - offset: jest.fn().mockReturnThis(), - }; - - db.mockReturnValue(mockQuery); - mockQuery.mockResolvedValue([]); - - const options = { outcome: 'attacker_victory' }; - await combatService.getCombatHistory(playerId, options, correlationId); - - // Verify that the outcome filter was applied - expect(mockQuery.where).toHaveBeenCalledWith('combat_encounters.outcome', 'attacker_victory'); - }); - }); - - describe('getActiveCombats', () => { - const playerId = 10; - const correlationId = 'test-correlation-id'; - - it('should return active combats for player', async () => { - const mockActiveCombats = [ - { - id: 1, - status: 'active', - battle_type: 'fleet_vs_fleet', - attacker_fleet_name: 'Attack Fleet', - }, - { - id: 2, - status: 'preparing', - battle_type: 'fleet_vs_colony', - defender_colony_name: 'Defended Colony', - }, - ]; - - const mockQuery = { - select: jest.fn().mockReturnThis(), - leftJoin: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - whereIn: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - }; - - db.mockReturnValue(mockQuery); - mockQuery.mockResolvedValue(mockActiveCombats); - - const result = await combatService.getActiveCombats(playerId, correlationId); - - expect(result).toHaveLength(2); - expect(result[0]).toHaveProperty('status', 'active'); - expect(result[1]).toHaveProperty('status', 'preparing'); - }); - - it('should return empty array when no active combats', async () => { - const mockQuery = { - select: jest.fn().mockReturnThis(), - leftJoin: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - whereIn: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - }; - - db.mockReturnValue(mockQuery); - mockQuery.mockResolvedValue([]); - - const result = await combatService.getActiveCombats(playerId, correlationId); - - expect(result).toHaveLength(0); - }); - }); - - describe('validateCombatInitiation', () => { - const correlationId = 'test-correlation-id'; - - beforeEach(() => { - // Mock database queries - const mockQuery = { - where: jest.fn().mockReturnThis(), - first: jest.fn(), - }; - db.mockReturnValue(mockQuery); - }); - - it('should validate successful fleet vs fleet combat', async () => { - const combatData = { - attacker_fleet_id: 1, - defender_fleet_id: 2, - location: 'A3-91-X', - }; - const attackerPlayerId = 10; - - // Mock attacker fleet - const mockAttackerFleet = { - id: 1, - player_id: 10, - current_location: 'A3-91-X', - fleet_status: 'idle', - }; - - // Mock defender fleet - const mockDefenderFleet = { - id: 2, - player_id: 20, - current_location: 'A3-91-X', - fleet_status: 'idle', - }; - - db().first - .mockResolvedValueOnce(mockAttackerFleet) // First call for attacker fleet - .mockResolvedValueOnce(mockDefenderFleet); // Second call for defender fleet - - await expect(combatService.validateCombatInitiation(combatData, attackerPlayerId, correlationId)) - .resolves - .not.toThrow(); - }); - - it('should reject combat with invalid attacker fleet', async () => { - const combatData = { - attacker_fleet_id: 999, - defender_fleet_id: 2, - location: 'A3-91-X', - }; - const attackerPlayerId = 10; - - db().first.mockResolvedValueOnce(null); // Attacker fleet not found - - await expect(combatService.validateCombatInitiation(combatData, attackerPlayerId, correlationId)) - .rejects - .toThrow('Invalid attacker fleet or fleet not available for combat'); - }); - - it('should reject combat when attacker fleet not at location', async () => { - const combatData = { - attacker_fleet_id: 1, - defender_fleet_id: 2, - location: 'A3-91-X', - }; - const attackerPlayerId = 10; - - const mockAttackerFleet = { - id: 1, - player_id: 10, - current_location: 'B2-50-Y', // Different location - fleet_status: 'idle', - }; - - db().first.mockResolvedValueOnce(mockAttackerFleet); - - await expect(combatService.validateCombatInitiation(combatData, attackerPlayerId, correlationId)) - .rejects - .toThrow('Fleet must be at the specified location to initiate combat'); - }); - - it('should reject combat against own fleet', async () => { - const combatData = { - attacker_fleet_id: 1, - defender_fleet_id: 2, - location: 'A3-91-X', - }; - const attackerPlayerId = 10; - - const mockAttackerFleet = { - id: 1, - player_id: 10, - current_location: 'A3-91-X', - fleet_status: 'idle', - }; - - const mockDefenderFleet = { - id: 2, - player_id: 10, // Same player as attacker - current_location: 'A3-91-X', - fleet_status: 'idle', - }; - - db().first - .mockResolvedValueOnce(mockAttackerFleet) - .mockResolvedValueOnce(mockDefenderFleet); - - await expect(combatService.validateCombatInitiation(combatData, attackerPlayerId, correlationId)) - .rejects - .toThrow('Cannot attack your own fleet'); - }); - }); - - describe('calculateCasualties', () => { - it('should calculate casualties for attacker victory', () => { - const forces = { - attacker: { - fleet: { - ships: [ - { design_name: 'Fighter', quantity: 50 }, - { design_name: 'Destroyer', quantity: 10 }, - ], - }, - }, - defender: { - fleet: { - ships: [ - { design_name: 'Cruiser', quantity: 20 }, - ], - }, - }, - }; - - const casualties = combatService.calculateCasualties(forces, true, 'test-correlation'); - - expect(casualties).toHaveProperty('attacker'); - expect(casualties).toHaveProperty('defender'); - expect(casualties.attacker.total_ships).toBeGreaterThanOrEqual(0); - expect(casualties.defender.total_ships).toBeGreaterThanOrEqual(0); - - // Attacker should have fewer casualties than defender - expect(casualties.attacker.total_ships).toBeLessThan(casualties.defender.total_ships); - }); - - it('should calculate casualties for defender victory', () => { - const forces = { - attacker: { - fleet: { - ships: [ - { design_name: 'Fighter', quantity: 30 }, - ], - }, - }, - defender: { - colony: { - defense_buildings: [ - { building_name: 'Defense Grid', health_percentage: 100 }, - ], - }, - }, - }; - - const casualties = combatService.calculateCasualties(forces, false, 'test-correlation'); - - expect(casualties.defender.total_ships).toBeLessThan(casualties.attacker.total_ships); - expect(casualties.defender.buildings).toEqual({}); // Colony defended successfully - }); - }); - - describe('calculateLoot', () => { - it('should calculate loot for attacker victory', () => { - const forces = { - attacker: { fleet: { id: 1 } }, - defender: { fleet: { id: 2 } }, - }; - - const loot = combatService.calculateLoot(forces, true, 'test-correlation'); - - expect(loot).toHaveProperty('scrap'); - expect(loot).toHaveProperty('energy'); - expect(loot.scrap).toBeGreaterThan(0); - expect(loot.energy).toBeGreaterThan(0); - }); - - it('should return empty loot for defender victory', () => { - const forces = { - attacker: { fleet: { id: 1 } }, - defender: { fleet: { id: 2 } }, - }; - - const loot = combatService.calculateLoot(forces, false, 'test-correlation'); - - expect(loot).toEqual({}); - }); - - it('should include bonus loot for colony raids', () => { - const forces = { - attacker: { fleet: { id: 1 } }, - defender: { colony: { id: 1 } }, - }; - - const loot = combatService.calculateLoot(forces, true, 'test-correlation'); - - expect(loot).toHaveProperty('scrap'); - expect(loot).toHaveProperty('energy'); - expect(loot).toHaveProperty('data_cores'); - expect(loot.data_cores).toBeGreaterThan(0); - }); - }); -}); diff --git a/src/tests/unit/services/game-tick.service.test.js b/src/tests/unit/services/game-tick.service.test.js deleted file mode 100644 index 3497cf5..0000000 --- a/src/tests/unit/services/game-tick.service.test.js +++ /dev/null @@ -1,687 +0,0 @@ -/** - * Game Tick Service Unit Tests - * Comprehensive tests for all game tick functionality including resource production, - * building construction, research progress, and fleet movements. - */ - -const GameTickService = require('../../../services/game-tick.service'); -const db = require('../../../database/connection'); -const redisClient = require('../../../utils/redis'); -const logger = require('../../../utils/logger'); - -// Mock dependencies -jest.mock('../../../database/connection'); -jest.mock('../../../utils/redis'); -jest.mock('../../../utils/logger'); -jest.mock('node-cron'); - -describe('GameTickService', () => { - let gameTickService; - let mockGameEventService; - - beforeEach(() => { - // Reset all mocks - jest.clearAllMocks(); - - // Mock GameEventService - mockGameEventService = { - emitResourcesUpdated: jest.fn(), - emitBuildingConstructed: jest.fn(), - emitNotification: jest.fn(), - emitErrorEvent: jest.fn(), - emitSystemAnnouncement: jest.fn(), - }; - - // Create service instance - gameTickService = new GameTickService(mockGameEventService); - - // Mock Redis operations - redisClient.setex = jest.fn().mockResolvedValue('OK'); - redisClient.get = jest.fn().mockResolvedValue(null); - redisClient.del = jest.fn().mockResolvedValue(1); - - // Mock database transaction - const mockTrx = { - players: jest.fn().mockReturnThis(), - colonies: jest.fn().mockReturnThis(), - colony_buildings: jest.fn().mockReturnThis(), - colony_resource_production: jest.fn().mockReturnThis(), - player_resources: jest.fn().mockReturnThis(), - resource_types: jest.fn().mockReturnThis(), - building_types: jest.fn().mockReturnThis(), - planet_types: jest.fn().mockReturnThis(), - fleets: jest.fn().mockReturnThis(), - player_research: jest.fn().mockReturnThis(), - technologies: jest.fn().mockReturnThis(), - research_facilities: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - join: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - first: jest.fn(), - orderBy: jest.fn().mockReturnThis(), - update: jest.fn().mockReturnThis(), - increment: jest.fn().mockReturnThis(), - decrement: jest.fn().mockReturnThis(), - insert: jest.fn().mockReturnThis(), - returning: jest.fn(), - raw: jest.fn(), - }; - - db.transaction = jest.fn().mockImplementation(async (callback) => { - return callback(mockTrx); - }); - - // Mock basic database queries - db.mockReturnValue(mockTrx); - }); - - describe('Constructor', () => { - it('should initialize with default values', () => { - const service = new GameTickService(); - - expect(service.isInitialized).toBe(false); - expect(service.currentTick).toBe(0); - expect(service.cronJob).toBe(null); - expect(service.config).toBe(null); - expect(service.isProcessing).toBe(false); - expect(service.failedUserGroups).toBeInstanceOf(Set); - expect(service.tickMetrics).toHaveProperty('totalTicksProcessed', 0); - }); - - it('should accept gameEventService parameter', () => { - const service = new GameTickService(mockGameEventService); - expect(service.gameEventService).toBe(mockGameEventService); - }); - }); - - describe('Configuration Management', () => { - it('should load existing configuration', async () => { - const mockConfig = { - id: 1, - tick_interval_ms: 60000, - user_groups_count: 10, - max_retry_attempts: 5, - bonus_tick_threshold: 3, - retry_delay_ms: 5000, - is_active: true, - }; - - db().where().first.mockResolvedValue(mockConfig); - - await gameTickService.loadConfig(); - - expect(gameTickService.config).toEqual(expect.objectContaining(mockConfig)); - }); - - it('should create default configuration if none exists', async () => { - const mockDefaultConfig = { - id: 1, - tick_interval_ms: 60000, - user_groups_count: 10, - max_retry_attempts: 5, - bonus_tick_threshold: 3, - retry_delay_ms: 5000, - is_active: true, - }; - - db().where().first.mockResolvedValue(null); - db().insert().returning.mockResolvedValue([mockDefaultConfig]); - - await gameTickService.loadConfig(); - - expect(gameTickService.config).toEqual(expect.objectContaining(mockDefaultConfig)); - expect(db().insert).toHaveBeenCalled(); - }); - }); - - describe('Player Tick Processing', () => { - beforeEach(() => { - gameTickService.config = { - user_groups_count: 10, - max_retry_attempts: 5, - retry_delay_ms: 5000, - }; - }); - - it('should process player tick successfully', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - - const mockPlayer = { - id: playerId, - username: 'testplayer', - last_tick_processed: 0, - }; - - // Mock database responses - db().where().first.mockResolvedValue(mockPlayer); - db().where().update.mockResolvedValue([]); - - // Mock processing methods - gameTickService.processResourceProduction = jest.fn().mockResolvedValue({ - status: 'success', - totalResourcesProduced: { scrap: 100, energy: 50 }, - }); - gameTickService.processBuildingConstruction = jest.fn().mockResolvedValue({ - status: 'success', - completedBuildings: [], - }); - gameTickService.processResearch = jest.fn().mockResolvedValue({ - status: 'success', - completedResearch: [], - }); - gameTickService.processFleetMovements = jest.fn().mockResolvedValue({ - status: 'success', - arrivedFleets: [], - }); - - await gameTickService.processPlayerTick(tickNumber, playerId, correlationId); - - // Verify all processing methods were called - expect(gameTickService.processResourceProduction).toHaveBeenCalledWith( - playerId, tickNumber, correlationId, expect.any(Object), - ); - expect(gameTickService.processBuildingConstruction).toHaveBeenCalledWith( - playerId, tickNumber, correlationId, expect.any(Object), - ); - expect(gameTickService.processResearch).toHaveBeenCalledWith( - playerId, tickNumber, correlationId, expect.any(Object), - ); - expect(gameTickService.processFleetMovements).toHaveBeenCalledWith( - playerId, tickNumber, correlationId, expect.any(Object), - ); - - // Verify player was updated - expect(db().where().update).toHaveBeenCalledWith( - expect.objectContaining({ - last_tick_processed: tickNumber, - }), - ); - }); - - it('should skip player if already processed', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - - const mockPlayer = { - id: playerId, - username: 'testplayer', - last_tick_processed: 2, // Already processed a later tick - }; - - db().where().first.mockResolvedValue(mockPlayer); - - gameTickService.processResourceProduction = jest.fn(); - - await gameTickService.processPlayerTick(tickNumber, playerId, correlationId); - - // Should not process if already handled - expect(gameTickService.processResourceProduction).not.toHaveBeenCalled(); - }); - - it('should handle player processing errors', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - - db().where().first.mockRejectedValue(new Error('Database error')); - - await expect( - gameTickService.processPlayerTick(tickNumber, playerId, correlationId), - ).rejects.toThrow('Database error'); - - expect(mockGameEventService.emitErrorEvent).toHaveBeenCalledWith( - playerId, - 'tick_processing_failed', - expect.any(String), - expect.any(Object), - correlationId, - ); - }); - }); - - describe('Resource Production Processing', () => { - beforeEach(() => { - gameTickService.addResourcesFromProduction = jest.fn().mockResolvedValue(); - gameTickService.processColonyResourceProduction = jest.fn(); - }); - - it('should process resource production for all colonies', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - const mockTrx = {}; - - const mockColonies = [ - { - id: 1, - name: 'Colony 1', - planet_type_name: 'Terran', - resource_modifiers: { scrap: 1.0, energy: 1.0 }, - }, - { - id: 2, - name: 'Colony 2', - planet_type_name: 'Industrial', - resource_modifiers: { scrap: 1.5, energy: 0.8 }, - }, - ]; - - db().select().join().where.mockResolvedValue(mockColonies); - - // Mock colony processing results - gameTickService.processColonyResourceProduction - .mockResolvedValueOnce({ - status: 'success', - resourcesProduced: { scrap: 50, energy: 25 }, - }) - .mockResolvedValueOnce({ - status: 'success', - resourcesProduced: { scrap: 75, energy: 20 }, - }); - - const result = await gameTickService.processResourceProduction( - playerId, tickNumber, correlationId, mockTrx, - ); - - expect(result.status).toBe('success'); - expect(result.coloniesProcessed).toBe(2); - expect(result.totalResourcesProduced).toEqual({ - scrap: 125, - energy: 45, - }); - - expect(gameTickService.addResourcesFromProduction).toHaveBeenCalledWith( - playerId, - { scrap: 125, energy: 45 }, - correlationId, - mockTrx, - ); - - expect(mockGameEventService.emitResourcesUpdated).toHaveBeenCalledWith( - playerId, - { scrap: 125, energy: 45 }, - 'production_tick', - correlationId, - ); - }); - - it('should handle empty colonies gracefully', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - const mockTrx = {}; - - db().select().join().where.mockResolvedValue([]); - - const result = await gameTickService.processResourceProduction( - playerId, tickNumber, correlationId, mockTrx, - ); - - expect(result.status).toBe('success'); - expect(result.message).toBe('No colonies to process'); - expect(result.resourcesProduced).toEqual({}); - }); - }); - - describe('Building Construction Processing', () => { - it('should complete buildings that are ready', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - const mockTrx = {}; - const currentTime = new Date(); - - const mockBuildings = [ - { - id: 1, - colony_id: 10, - building_type_id: 1, - level: 2, - colony_name: 'Test Colony', - building_name: 'Power Plant', - construction_completes: new Date(currentTime.getTime() - 1000), // Completed 1 second ago - }, - ]; - - db().select().join().where.mockResolvedValue(mockBuildings); - db().where().update.mockResolvedValue([]); - - gameTickService.updateColonyProductionRates = jest.fn().mockResolvedValue(); - - const result = await gameTickService.processBuildingConstruction( - playerId, tickNumber, correlationId, mockTrx, - ); - - expect(result.status).toBe('success'); - expect(result.completedBuildings).toHaveLength(1); - expect(result.completedBuildings[0]).toEqual( - expect.objectContaining({ - buildingId: 1, - buildingName: 'Power Plant', - colonyId: 10, - colonyName: 'Test Colony', - level: 2, - }), - ); - - expect(mockGameEventService.emitBuildingConstructed).toHaveBeenCalledWith( - playerId, - 10, - expect.objectContaining({ id: 1, level: 2 }), - correlationId, - ); - - expect(mockGameEventService.emitNotification).toHaveBeenCalledWith( - playerId, - expect.objectContaining({ - type: 'building_completed', - title: 'Construction Complete', - }), - correlationId, - ); - }); - - it('should handle no buildings ready for completion', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - const mockTrx = {}; - - db().select().join().where.mockResolvedValue([]); - - const result = await gameTickService.processBuildingConstruction( - playerId, tickNumber, correlationId, mockTrx, - ); - - expect(result.status).toBe('success'); - expect(result.message).toBe('No buildings ready for completion'); - expect(result.completedBuildings).toHaveLength(0); - }); - }); - - describe('Research Processing', () => { - beforeEach(() => { - gameTickService.calculateResearchBonus = jest.fn().mockResolvedValue(0.1); - }); - - it('should complete research that is finished', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - const mockTrx = {}; - const currentTime = new Date(); - - const mockResearch = [ - { - id: 1, - technology_id: 10, - technology_name: 'Advanced Mining', - research_time: 60, // 60 minutes - started_at: new Date(currentTime.getTime() - 70 * 60 * 1000), // Started 70 minutes ago - effects: { mining_efficiency: 1.2 }, - }, - ]; - - db().select().join().where.mockResolvedValue(mockResearch); - db().where().update.mockResolvedValue([]); - - const result = await gameTickService.processResearch( - playerId, tickNumber, correlationId, mockTrx, - ); - - expect(result.status).toBe('success'); - expect(result.completedResearch).toHaveLength(1); - expect(result.completedResearch[0]).toEqual( - expect.objectContaining({ - technologyName: 'Advanced Mining', - effects: { mining_efficiency: 1.2 }, - }), - ); - - expect(mockGameEventService.emitNotification).toHaveBeenCalledWith( - playerId, - expect.objectContaining({ - type: 'research_completed', - title: 'Research Complete', - }), - correlationId, - ); - }); - - it('should update progress for ongoing research', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - const mockTrx = {}; - const currentTime = new Date(); - - const mockResearch = [ - { - id: 1, - technology_id: 10, - technology_name: 'Quantum Computing', - research_time: 120, // 120 minutes - started_at: new Date(currentTime.getTime() - 60 * 60 * 1000), // Started 60 minutes ago - effects: {}, - }, - ]; - - db().select().join().where.mockResolvedValue(mockResearch); - db().where().update.mockResolvedValue([]); - - const result = await gameTickService.processResearch( - playerId, tickNumber, correlationId, mockTrx, - ); - - expect(result.status).toBe('success'); - expect(result.completedResearch).toHaveLength(0); - - // Should update progress (approximately 50% with bonus) - expect(db().where().update).toHaveBeenCalledWith( - expect.objectContaining({ - progress: expect.any(Number), - }), - ); - }); - }); - - describe('Fleet Movement Processing', () => { - it('should move fleets that have arrived', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - const mockTrx = {}; - const currentTime = new Date(); - - const mockFleets = [ - { - id: 1, - name: 'Exploration Fleet', - destination: 'B2-45-C', - arrival_time: new Date(currentTime.getTime() - 1000), // Arrived 1 second ago - }, - ]; - - db().where.mockResolvedValue(mockFleets); - db().where().update.mockResolvedValue([]); - - const result = await gameTickService.processFleetMovements( - playerId, tickNumber, correlationId, mockTrx, - ); - - expect(result.status).toBe('success'); - expect(result.arrivedFleets).toHaveLength(1); - expect(result.arrivedFleets[0]).toEqual( - expect.objectContaining({ - fleetName: 'Exploration Fleet', - destination: 'B2-45-C', - }), - ); - - expect(mockGameEventService.emitNotification).toHaveBeenCalledWith( - playerId, - expect.objectContaining({ - type: 'fleet_arrived', - title: 'Fleet Arrived', - }), - correlationId, - ); - }); - - it('should handle no fleets arriving', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - const mockTrx = {}; - - db().where.mockResolvedValue([]); - - const result = await gameTickService.processFleetMovements( - playerId, tickNumber, correlationId, mockTrx, - ); - - expect(result.status).toBe('success'); - expect(result.message).toBe('No fleets arriving'); - expect(result.arrivedFleets).toHaveLength(0); - }); - }); - - describe('Colony Resource Production Calculations', () => { - it('should calculate production with building and planet modifiers', async () => { - const colony = { - id: 1, - name: 'Test Colony', - resource_modifiers: { scrap: 1.5, energy: 0.8 }, - }; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - const mockTrx = {}; - - const mockBuildings = [ - { - id: 1, - level: 3, - building_name: 'Salvage Yard', - base_production: { scrap: 10 }, - production_multiplier: 1.2, - }, - { - id: 2, - level: 2, - building_name: 'Power Plant', - base_production: { energy: 8 }, - production_multiplier: 1.2, - }, - ]; - - db().select().join().where.mockResolvedValue(mockBuildings); - - gameTickService.updateColonyResourceTracking = jest.fn().mockResolvedValue(); - - const result = await gameTickService.processColonyResourceProduction( - colony, tickNumber, correlationId, mockTrx, - ); - - expect(result.status).toBe('success'); - expect(result.buildingsProcessed).toBe(2); - - // Verify production calculations - // Salvage Yard: 10 * 1.2^2 * 1.5 = 21.6 -> 21 - // Power Plant: 8 * 1.2^1 * 0.8 = 7.68 -> 7 - expect(result.resourcesProduced.scrap).toBeGreaterThan(0); - expect(result.resourcesProduced.energy).toBeGreaterThan(0); - }); - }); - - describe('Error Handling and Recovery', () => { - it('should handle database transaction failures', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - - db.transaction.mockRejectedValue(new Error('Transaction failed')); - - await expect( - gameTickService.processPlayerTick(tickNumber, playerId, correlationId), - ).rejects.toThrow('Transaction failed'); - - expect(logger.error).toHaveBeenCalledWith( - 'Player tick processing failed', - expect.objectContaining({ - correlationId, - playerId, - tickNumber, - error: 'Transaction failed', - }), - ); - }); - - it('should release Redis locks even on failure', async () => { - const playerId = 123; - const tickNumber = 1; - const correlationId = 'test-correlation-id'; - - redisClient.setex.mockResolvedValue('OK'); - db.transaction.mockRejectedValue(new Error('Database error')); - - await expect( - gameTickService.processPlayerTick(tickNumber, playerId, correlationId), - ).rejects.toThrow('Database error'); - - expect(redisClient.del).toHaveBeenCalled(); - }); - }); - - describe('Performance Metrics', () => { - it('should track processing metrics', () => { - expect(gameTickService.tickMetrics).toHaveProperty('totalTicksProcessed'); - expect(gameTickService.tickMetrics).toHaveProperty('totalPlayersProcessed'); - expect(gameTickService.tickMetrics).toHaveProperty('averageTickDuration'); - expect(gameTickService.tickMetrics).toHaveProperty('consecutiveFailures'); - }); - - it('should provide comprehensive status information', () => { - const status = gameTickService.getStatus(); - - expect(status).toHaveProperty('initialized'); - expect(status).toHaveProperty('currentTick'); - expect(status).toHaveProperty('running'); - expect(status).toHaveProperty('processing'); - expect(status).toHaveProperty('config'); - expect(status).toHaveProperty('metrics'); - expect(status).toHaveProperty('failedUserGroups'); - }); - }); - - describe('Bonus Tick Compensation', () => { - it('should apply bonus resources for failed ticks', async () => { - const tickNumber = 1; - const userGroup = 0; - const correlationId = 'test-correlation-id'; - - const mockPlayers = [ - { id: 1, username: 'player1' }, - { id: 2, username: 'player2' }, - ]; - - db().where().update().mockResolvedValue([]); - db().where().select.mockResolvedValue(mockPlayers); - db().join().where().increment().update.mockResolvedValue([]); - - await gameTickService.applyBonusTick(tickNumber, userGroup, correlationId); - - expect(mockGameEventService.emitNotification).toHaveBeenCalledTimes(2); - expect(mockGameEventService.emitNotification).toHaveBeenCalledWith( - expect.any(Number), - expect.objectContaining({ - type: 'bonus_compensation', - title: 'System Compensation', - }), - correlationId, - ); - }); - }); -}); diff --git a/src/utils/jwt.js b/src/utils/jwt.js index 560828b..baa60a8 100644 --- a/src/utils/jwt.js +++ b/src/utils/jwt.js @@ -8,38 +8,38 @@ const logger = require('./logger'); // JWT Configuration const JWT_CONFIG = { - player: { - secret: process.env.JWT_PLAYER_SECRET || 'player-secret-change-in-production', - expiresIn: process.env.JWT_PLAYER_EXPIRES_IN || '24h', - issuer: process.env.JWT_ISSUER || 'shattered-void-mmo', - audience: 'player', - }, - admin: { - secret: process.env.JWT_ADMIN_SECRET || 'admin-secret-change-in-production', - expiresIn: process.env.JWT_ADMIN_EXPIRES_IN || '8h', - issuer: process.env.JWT_ISSUER || 'shattered-void-mmo', - audience: 'admin', - }, - refresh: { - secret: process.env.JWT_REFRESH_SECRET || 'refresh-secret-change-in-production', - expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d', - issuer: process.env.JWT_ISSUER || 'shattered-void-mmo', - }, + player: { + secret: process.env.JWT_PLAYER_SECRET || 'player-secret-change-in-production', + expiresIn: process.env.JWT_PLAYER_EXPIRES_IN || '24h', + issuer: process.env.JWT_ISSUER || 'shattered-void-mmo', + audience: 'player' + }, + admin: { + secret: process.env.JWT_ADMIN_SECRET || 'admin-secret-change-in-production', + expiresIn: process.env.JWT_ADMIN_EXPIRES_IN || '8h', + issuer: process.env.JWT_ISSUER || 'shattered-void-mmo', + audience: 'admin' + }, + refresh: { + secret: process.env.JWT_REFRESH_SECRET || 'refresh-secret-change-in-production', + expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d', + issuer: process.env.JWT_ISSUER || 'shattered-void-mmo' + } }; // Validate JWT secrets in production if (process.env.NODE_ENV === 'production') { - const defaultSecrets = [ - 'player-secret-change-in-production', - 'admin-secret-change-in-production', - 'refresh-secret-change-in-production', - ]; + const defaultSecrets = [ + 'player-secret-change-in-production', + 'admin-secret-change-in-production', + 'refresh-secret-change-in-production' + ]; - if (defaultSecrets.includes(JWT_CONFIG.player.secret) || + if (defaultSecrets.includes(JWT_CONFIG.player.secret) || defaultSecrets.includes(JWT_CONFIG.admin.secret) || defaultSecrets.includes(JWT_CONFIG.refresh.secret)) { - throw new Error('Default JWT secrets detected in production environment. Please set proper JWT secrets.'); - } + throw new Error('Default JWT secrets detected in production environment. Please set proper JWT secrets.'); + } } /** @@ -52,40 +52,40 @@ if (process.env.NODE_ENV === 'production') { * @returns {string} JWT token */ function generatePlayerToken(payload, options = {}) { - try { - const tokenPayload = { - playerId: payload.playerId, - email: payload.email, - username: payload.username, - type: 'player', - iat: Math.floor(Date.now() / 1000), - ...options.extraPayload, - }; + try { + const tokenPayload = { + playerId: payload.playerId, + email: payload.email, + username: payload.username, + type: 'player', + iat: Math.floor(Date.now() / 1000), + ...options.extraPayload + }; - const tokenOptions = { - expiresIn: options.expiresIn || JWT_CONFIG.player.expiresIn, - issuer: JWT_CONFIG.player.issuer, - audience: JWT_CONFIG.player.audience, - subject: payload.playerId.toString(), - }; + const tokenOptions = { + expiresIn: options.expiresIn || JWT_CONFIG.player.expiresIn, + issuer: JWT_CONFIG.player.issuer, + audience: JWT_CONFIG.player.audience, + subject: payload.playerId.toString() + }; - const token = jwt.sign(tokenPayload, JWT_CONFIG.player.secret, tokenOptions); + const token = jwt.sign(tokenPayload, JWT_CONFIG.player.secret, tokenOptions); - logger.info('Player JWT token generated', { - playerId: payload.playerId, - username: payload.username, - expiresIn: tokenOptions.expiresIn, - }); + logger.info('Player JWT token generated', { + playerId: payload.playerId, + username: payload.username, + expiresIn: tokenOptions.expiresIn + }); - return token; + return token; - } catch (error) { - logger.error('Failed to generate player JWT token:', { - playerId: payload.playerId, - error: error.message, - }); - throw new Error('Token generation failed'); - } + } catch (error) { + logger.error('Failed to generate player JWT token:', { + playerId: payload.playerId, + error: error.message + }); + throw new Error('Token generation failed'); + } } /** @@ -99,42 +99,42 @@ function generatePlayerToken(payload, options = {}) { * @returns {string} JWT token */ function generateAdminToken(payload, options = {}) { - try { - const tokenPayload = { - adminId: payload.adminId, - email: payload.email, - username: payload.username, - permissions: payload.permissions || [], - type: 'admin', - iat: Math.floor(Date.now() / 1000), - ...options.extraPayload, - }; + try { + const tokenPayload = { + adminId: payload.adminId, + email: payload.email, + username: payload.username, + permissions: payload.permissions || [], + type: 'admin', + iat: Math.floor(Date.now() / 1000), + ...options.extraPayload + }; - const tokenOptions = { - expiresIn: options.expiresIn || JWT_CONFIG.admin.expiresIn, - issuer: JWT_CONFIG.admin.issuer, - audience: JWT_CONFIG.admin.audience, - subject: payload.adminId.toString(), - }; + const tokenOptions = { + expiresIn: options.expiresIn || JWT_CONFIG.admin.expiresIn, + issuer: JWT_CONFIG.admin.issuer, + audience: JWT_CONFIG.admin.audience, + subject: payload.adminId.toString() + }; - const token = jwt.sign(tokenPayload, JWT_CONFIG.admin.secret, tokenOptions); + const token = jwt.sign(tokenPayload, JWT_CONFIG.admin.secret, tokenOptions); - logger.info('Admin JWT token generated', { - adminId: payload.adminId, - username: payload.username, - permissions: payload.permissions, - expiresIn: tokenOptions.expiresIn, - }); + logger.info('Admin JWT token generated', { + adminId: payload.adminId, + username: payload.username, + permissions: payload.permissions, + expiresIn: tokenOptions.expiresIn + }); - return token; + return token; - } catch (error) { - logger.error('Failed to generate admin JWT token:', { - adminId: payload.adminId, - error: error.message, - }); - throw new Error('Token generation failed'); - } + } catch (error) { + logger.error('Failed to generate admin JWT token:', { + adminId: payload.adminId, + error: error.message + }); + throw new Error('Token generation failed'); + } } /** @@ -146,40 +146,40 @@ function generateAdminToken(payload, options = {}) { * @returns {string} Refresh token */ function generateRefreshToken(payload, options = {}) { - try { - const tokenPayload = { - userId: payload.userId, - type: payload.type, - tokenId: require('uuid').v4(), - iat: Math.floor(Date.now() / 1000), - }; + try { + const tokenPayload = { + userId: payload.userId, + type: payload.type, + tokenId: require('uuid').v4(), + iat: Math.floor(Date.now() / 1000) + }; - const tokenOptions = { - expiresIn: options.expiresIn || JWT_CONFIG.refresh.expiresIn, - issuer: JWT_CONFIG.refresh.issuer, - audience: payload.type, - subject: payload.userId.toString(), - }; + const tokenOptions = { + expiresIn: options.expiresIn || JWT_CONFIG.refresh.expiresIn, + issuer: JWT_CONFIG.refresh.issuer, + audience: payload.type, + subject: payload.userId.toString() + }; - const token = jwt.sign(tokenPayload, JWT_CONFIG.refresh.secret, tokenOptions); + const token = jwt.sign(tokenPayload, JWT_CONFIG.refresh.secret, tokenOptions); - logger.info('Refresh token generated', { - userId: payload.userId, - type: payload.type, - tokenId: tokenPayload.tokenId, - expiresIn: tokenOptions.expiresIn, - }); + logger.info('Refresh token generated', { + userId: payload.userId, + type: payload.type, + tokenId: tokenPayload.tokenId, + expiresIn: tokenOptions.expiresIn + }); - return token; + return token; - } catch (error) { - logger.error('Failed to generate refresh token:', { - userId: payload.userId, - type: payload.type, - error: error.message, - }); - throw new Error('Refresh token generation failed'); - } + } catch (error) { + logger.error('Failed to generate refresh token:', { + userId: payload.userId, + type: payload.type, + error: error.message + }); + throw new Error('Refresh token generation failed'); + } } /** @@ -188,32 +188,32 @@ function generateRefreshToken(payload, options = {}) { * @returns {Object} Decoded token payload */ function verifyPlayerToken(token) { - try { - const decoded = jwt.verify(token, JWT_CONFIG.player.secret, { - issuer: JWT_CONFIG.player.issuer, - audience: JWT_CONFIG.player.audience, - }); + try { + const decoded = jwt.verify(token, JWT_CONFIG.player.secret, { + issuer: JWT_CONFIG.player.issuer, + audience: JWT_CONFIG.player.audience + }); - if (decoded.type !== 'player') { - throw new Error('Invalid token type'); + if (decoded.type !== 'player') { + throw new Error('Invalid token type'); + } + + return decoded; + + } catch (error) { + logger.warn('Player JWT token verification failed:', { + error: error.message, + tokenPrefix: token ? token.substring(0, 20) + '...' : 'null' + }); + + if (error.name === 'TokenExpiredError') { + throw new Error('Token expired'); + } else if (error.name === 'JsonWebTokenError') { + throw new Error('Invalid token'); + } else { + throw new Error('Token verification failed'); + } } - - return decoded; - - } catch (error) { - logger.warn('Player JWT token verification failed:', { - error: error.message, - tokenPrefix: token ? token.substring(0, 20) + '...' : 'null', - }); - - if (error.name === 'TokenExpiredError') { - throw new Error('Token expired'); - } else if (error.name === 'JsonWebTokenError') { - throw new Error('Invalid token'); - } else { - throw new Error('Token verification failed'); - } - } } /** @@ -222,32 +222,32 @@ function verifyPlayerToken(token) { * @returns {Object} Decoded token payload */ function verifyAdminToken(token) { - try { - const decoded = jwt.verify(token, JWT_CONFIG.admin.secret, { - issuer: JWT_CONFIG.admin.issuer, - audience: JWT_CONFIG.admin.audience, - }); + try { + const decoded = jwt.verify(token, JWT_CONFIG.admin.secret, { + issuer: JWT_CONFIG.admin.issuer, + audience: JWT_CONFIG.admin.audience + }); - if (decoded.type !== 'admin') { - throw new Error('Invalid token type'); + if (decoded.type !== 'admin') { + throw new Error('Invalid token type'); + } + + return decoded; + + } catch (error) { + logger.warn('Admin JWT token verification failed:', { + error: error.message, + tokenPrefix: token ? token.substring(0, 20) + '...' : 'null' + }); + + if (error.name === 'TokenExpiredError') { + throw new Error('Token expired'); + } else if (error.name === 'JsonWebTokenError') { + throw new Error('Invalid token'); + } else { + throw new Error('Token verification failed'); + } } - - return decoded; - - } catch (error) { - logger.warn('Admin JWT token verification failed:', { - error: error.message, - tokenPrefix: token ? token.substring(0, 20) + '...' : 'null', - }); - - if (error.name === 'TokenExpiredError') { - throw new Error('Token expired'); - } else if (error.name === 'JsonWebTokenError') { - throw new Error('Invalid token'); - } else { - throw new Error('Token verification failed'); - } - } } /** @@ -256,27 +256,27 @@ function verifyAdminToken(token) { * @returns {Object} Decoded token payload */ function verifyRefreshToken(token) { - try { - const decoded = jwt.verify(token, JWT_CONFIG.refresh.secret, { - issuer: JWT_CONFIG.refresh.issuer, - }); + try { + const decoded = jwt.verify(token, JWT_CONFIG.refresh.secret, { + issuer: JWT_CONFIG.refresh.issuer + }); - return decoded; + return decoded; - } catch (error) { - logger.warn('Refresh token verification failed:', { - error: error.message, - tokenPrefix: token ? token.substring(0, 20) + '...' : 'null', - }); + } catch (error) { + logger.warn('Refresh token verification failed:', { + error: error.message, + tokenPrefix: token ? token.substring(0, 20) + '...' : 'null' + }); - if (error.name === 'TokenExpiredError') { - throw new Error('Refresh token expired'); - } else if (error.name === 'JsonWebTokenError') { - throw new Error('Invalid refresh token'); - } else { - throw new Error('Refresh token verification failed'); + if (error.name === 'TokenExpiredError') { + throw new Error('Refresh token expired'); + } else if (error.name === 'JsonWebTokenError') { + throw new Error('Invalid refresh token'); + } else { + throw new Error('Refresh token verification failed'); + } } - } } /** @@ -285,15 +285,15 @@ function verifyRefreshToken(token) { * @returns {Object} Decoded token payload */ function decodeToken(token) { - try { - return jwt.decode(token, { complete: true }); - } catch (error) { - logger.error('Failed to decode JWT token:', { - error: error.message, - tokenPrefix: token ? token.substring(0, 20) + '...' : 'null', - }); - return null; - } + try { + return jwt.decode(token, { complete: true }); + } catch (error) { + logger.error('Failed to decode JWT token:', { + error: error.message, + tokenPrefix: token ? token.substring(0, 20) + '...' : 'null' + }); + return null; + } } /** @@ -302,14 +302,14 @@ function decodeToken(token) { * @returns {boolean} True if token is expired */ function isTokenExpired(token) { - try { - const decoded = jwt.decode(token); - if (!decoded || !decoded.exp) return true; - - return Date.now() >= decoded.exp * 1000; - } catch (error) { - return true; - } + try { + const decoded = jwt.decode(token); + if (!decoded || !decoded.exp) return true; + + return Date.now() >= decoded.exp * 1000; + } catch (error) { + return true; + } } /** @@ -318,25 +318,25 @@ function isTokenExpired(token) { * @returns {string|null} JWT token or null if not found */ function extractTokenFromHeader(authHeader) { - if (!authHeader) return null; + if (!authHeader) return null; - const parts = authHeader.split(' '); - if (parts.length !== 2 || parts[0] !== 'Bearer') { - return null; - } + const parts = authHeader.split(' '); + if (parts.length !== 2 || parts[0] !== 'Bearer') { + return null; + } - return parts[1]; + return parts[1]; } module.exports = { - generatePlayerToken, - generateAdminToken, - generateRefreshToken, - verifyPlayerToken, - verifyAdminToken, - verifyRefreshToken, - decodeToken, - isTokenExpired, - extractTokenFromHeader, - JWT_CONFIG, -}; + generatePlayerToken, + generateAdminToken, + generateRefreshToken, + verifyPlayerToken, + verifyAdminToken, + verifyRefreshToken, + decodeToken, + isTokenExpired, + extractTokenFromHeader, + JWT_CONFIG +}; \ No newline at end of file diff --git a/src/utils/logger.js b/src/utils/logger.js index c6abca3..eba7221 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -7,7 +7,7 @@ const logDir = path.join(__dirname, '../../logs'); const logFormat = winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), - winston.format.json(), + winston.format.json() ); const consoleFormat = winston.format.combine( @@ -19,7 +19,7 @@ const consoleFormat = winston.format.combine( log += ` ${JSON.stringify(meta)}`; } return log; - }), + }) ); const logger = winston.createLogger({ @@ -51,7 +51,7 @@ const logger = winston.createLogger({ winston.format.json(), winston.format((info) => { return info.audit ? info : false; - })(), + })() ), }), ], @@ -71,4 +71,4 @@ logger.audit = (message, meta = {}) => { logger.info(message, { ...meta, audit: true }); }; -module.exports = logger; +module.exports = logger; \ No newline at end of file diff --git a/src/utils/password.js b/src/utils/password.js index 28a2cf9..d714864 100644 --- a/src/utils/password.js +++ b/src/utils/password.js @@ -6,18 +6,16 @@ const bcrypt = require('bcrypt'); const logger = require('./logger'); -// Configuration - relaxed password requirements +// Configuration const BCRYPT_CONFIG = { - saltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS) || 12, - maxPasswordLength: parseInt(process.env.MAX_PASSWORD_LENGTH) || 128, - minPasswordLength: parseInt(process.env.MIN_PASSWORD_LENGTH) || 6, + saltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS) || 12, + maxPasswordLength: parseInt(process.env.MAX_PASSWORD_LENGTH) || 128, + minPasswordLength: parseInt(process.env.MIN_PASSWORD_LENGTH) || 8 }; -// Configuration loaded successfully - // Validate salt rounds configuration if (BCRYPT_CONFIG.saltRounds < 10) { - logger.warn('Low bcrypt salt rounds detected. Consider using 12 or higher for production.'); + logger.warn('Low bcrypt salt rounds detected. Consider using 12 or higher for production.'); } /** @@ -26,37 +24,37 @@ if (BCRYPT_CONFIG.saltRounds < 10) { * @returns {Promise} Hashed password */ async function hashPassword(password) { - try { - if (!password || typeof password !== 'string') { - throw new Error('Password must be a non-empty string'); + try { + if (!password || typeof password !== 'string') { + throw new Error('Password must be a non-empty string'); + } + + if (password.length > BCRYPT_CONFIG.maxPasswordLength) { + throw new Error(`Password exceeds maximum length of ${BCRYPT_CONFIG.maxPasswordLength} characters`); + } + + if (password.length < BCRYPT_CONFIG.minPasswordLength) { + throw new Error(`Password must be at least ${BCRYPT_CONFIG.minPasswordLength} characters long`); + } + + const startTime = Date.now(); + const hashedPassword = await bcrypt.hash(password, BCRYPT_CONFIG.saltRounds); + const duration = Date.now() - startTime; + + logger.info('Password hashed successfully', { + duration: `${duration}ms`, + saltRounds: BCRYPT_CONFIG.saltRounds + }); + + return hashedPassword; + + } catch (error) { + logger.error('Password hashing failed:', { + error: error.message, + passwordLength: password?.length + }); + throw error; } - - if (password.length > BCRYPT_CONFIG.maxPasswordLength) { - throw new Error(`Password exceeds maximum length of ${BCRYPT_CONFIG.maxPasswordLength} characters`); - } - - if (password.length < BCRYPT_CONFIG.minPasswordLength) { - throw new Error(`Password must be at least ${BCRYPT_CONFIG.minPasswordLength} characters long`); - } - - const startTime = Date.now(); - const hashedPassword = await bcrypt.hash(password, BCRYPT_CONFIG.saltRounds); - const duration = Date.now() - startTime; - - logger.info('Password hashed successfully', { - duration: `${duration}ms`, - saltRounds: BCRYPT_CONFIG.saltRounds, - }); - - return hashedPassword; - - } catch (error) { - logger.error('Password hashing failed:', { - error: error.message, - passwordLength: password?.length, - }); - throw error; - } } /** @@ -66,34 +64,34 @@ async function hashPassword(password) { * @returns {Promise} True if password matches hash */ async function verifyPassword(password, hash) { - try { - if (!password || typeof password !== 'string') { - throw new Error('Password must be a non-empty string'); + try { + if (!password || typeof password !== 'string') { + throw new Error('Password must be a non-empty string'); + } + + if (!hash || typeof hash !== 'string') { + throw new Error('Hash must be a non-empty string'); + } + + const startTime = Date.now(); + const isValid = await bcrypt.compare(password, hash); + const duration = Date.now() - startTime; + + logger.info('Password verification completed', { + duration: `${duration}ms`, + isValid + }); + + return isValid; + + } catch (error) { + logger.error('Password verification failed:', { + error: error.message, + passwordLength: password?.length, + hashLength: hash?.length + }); + return false; } - - if (!hash || typeof hash !== 'string') { - throw new Error('Hash must be a non-empty string'); - } - - const startTime = Date.now(); - const isValid = await bcrypt.compare(password, hash); - const duration = Date.now() - startTime; - - logger.info('Password verification completed', { - duration: `${duration}ms`, - isValid, - }); - - return isValid; - - } catch (error) { - logger.error('Password verification failed:', { - error: error.message, - passwordLength: password?.length, - hashLength: hash?.length, - }); - return false; - } } /** @@ -102,33 +100,33 @@ async function verifyPassword(password, hash) { * @returns {boolean} True if hash needs to be updated */ function needsRehash(hash) { - try { - if (!hash || typeof hash !== 'string') { - return true; + try { + if (!hash || typeof hash !== 'string') { + return true; + } + + // Extract salt rounds from hash + const hashParts = hash.split('$'); + if (hashParts.length < 4) { + return true; + } + + const currentRounds = parseInt(hashParts[2]); + const needsUpdate = currentRounds !== BCRYPT_CONFIG.saltRounds; + + if (needsUpdate) { + logger.info('Password hash needs update', { + currentRounds, + targetRounds: BCRYPT_CONFIG.saltRounds + }); + } + + return needsUpdate; + + } catch (error) { + logger.error('Error checking if hash needs rehash:', error); + return true; } - - // Extract salt rounds from hash - const hashParts = hash.split('$'); - if (hashParts.length < 4) { - return true; - } - - const currentRounds = parseInt(hashParts[2]); - const needsUpdate = currentRounds !== BCRYPT_CONFIG.saltRounds; - - if (needsUpdate) { - logger.info('Password hash needs update', { - currentRounds, - targetRounds: BCRYPT_CONFIG.saltRounds, - }); - } - - return needsUpdate; - - } catch (error) { - logger.error('Error checking if hash needs rehash:', error); - return true; - } } /** @@ -138,29 +136,29 @@ function needsRehash(hash) { * @returns {Promise} Result object with hash and wasRehashed flag */ async function rehashIfNeeded(password, currentHash) { - try { - const shouldRehash = needsRehash(currentHash); + try { + const shouldRehash = needsRehash(currentHash); + + if (!shouldRehash) { + return { + hash: currentHash, + wasRehashed: false + }; + } - if (!shouldRehash) { - return { - hash: currentHash, - wasRehashed: false, - }; + const newHash = await hashPassword(password); + + logger.info('Password rehashed with updated salt rounds'); + + return { + hash: newHash, + wasRehashed: true + }; + + } catch (error) { + logger.error('Password rehashing failed:', error); + throw error; } - - const newHash = await hashPassword(password); - - logger.info('Password rehashed with updated salt rounds'); - - return { - hash: newHash, - wasRehashed: true, - }; - - } catch (error) { - logger.error('Password rehashing failed:', error); - throw error; - } } /** @@ -169,108 +167,108 @@ async function rehashIfNeeded(password, currentHash) { * @returns {Object} Validation result with isValid flag and errors array */ function validatePasswordStrength(password) { - const result = { - isValid: true, - errors: [], - score: 0, - requirements: { - minLength: false, - hasUppercase: false, - hasLowercase: false, - hasNumbers: false, - hasSpecialChars: false, - noCommonPatterns: true, - }, - }; + const result = { + isValid: true, + errors: [], + score: 0, + requirements: { + minLength: false, + hasUppercase: false, + hasLowercase: false, + hasNumbers: false, + hasSpecialChars: false, + noCommonPatterns: true + } + }; - if (!password || typeof password !== 'string') { - result.isValid = false; - result.errors.push('Password must be a string'); - return result; - } - - // Check minimum length - if (password.length >= BCRYPT_CONFIG.minPasswordLength) { - result.requirements.minLength = true; - result.score += 1; - } else { - result.isValid = false; - result.errors.push(`Password must be at least ${BCRYPT_CONFIG.minPasswordLength} characters long`); - } - - // Check maximum length - if (password.length > BCRYPT_CONFIG.maxPasswordLength) { - result.isValid = false; - result.errors.push(`Password exceeds maximum length of ${BCRYPT_CONFIG.maxPasswordLength} characters`); - return result; - } - - // Check for uppercase letters - if (/[A-Z]/.test(password)) { - result.requirements.hasUppercase = true; - result.score += 1; - } else { - result.errors.push('Password must contain at least one uppercase letter'); - } - - // Check for lowercase letters - if (/[a-z]/.test(password)) { - result.requirements.hasLowercase = true; - result.score += 1; - } else { - result.errors.push('Password must contain at least one lowercase letter'); - } - - // Check for numbers - if (/\d/.test(password)) { - result.requirements.hasNumbers = true; - result.score += 1; - } else { - result.errors.push('Password must contain at least one number'); - } - - // Check for special characters - if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) { - result.requirements.hasSpecialChars = true; - result.score += 1; - } else { - result.errors.push('Password must contain at least one special character'); - } - - // Check for common patterns - const commonPatterns = [ - /123456/, - /password/i, - /qwerty/i, - /admin/i, - /letmein/i, - ]; - - for (const pattern of commonPatterns) { - if (pattern.test(password)) { - result.requirements.noCommonPatterns = false; - result.errors.push('Password contains common patterns that are easily guessable'); - break; + if (!password || typeof password !== 'string') { + result.isValid = false; + result.errors.push('Password must be a string'); + return result; } - } - // Check for repeated characters - if (/(.)\1{2,}/.test(password)) { - result.errors.push('Password should not contain repeated characters'); - } - - // Final validation based on environment - const isProduction = process.env.NODE_ENV === 'production'; - const minimumScore = isProduction ? 4 : 3; // Stricter requirements in production - - if (result.score < minimumScore) { - result.isValid = false; - if (result.errors.length === 0) { - result.errors.push('Password does not meet strength requirements'); + // Check minimum length + if (password.length >= BCRYPT_CONFIG.minPasswordLength) { + result.requirements.minLength = true; + result.score += 1; + } else { + result.isValid = false; + result.errors.push(`Password must be at least ${BCRYPT_CONFIG.minPasswordLength} characters long`); } - } - return result; + // Check maximum length + if (password.length > BCRYPT_CONFIG.maxPasswordLength) { + result.isValid = false; + result.errors.push(`Password exceeds maximum length of ${BCRYPT_CONFIG.maxPasswordLength} characters`); + return result; + } + + // Check for uppercase letters + if (/[A-Z]/.test(password)) { + result.requirements.hasUppercase = true; + result.score += 1; + } else { + result.errors.push('Password must contain at least one uppercase letter'); + } + + // Check for lowercase letters + if (/[a-z]/.test(password)) { + result.requirements.hasLowercase = true; + result.score += 1; + } else { + result.errors.push('Password must contain at least one lowercase letter'); + } + + // Check for numbers + if (/\d/.test(password)) { + result.requirements.hasNumbers = true; + result.score += 1; + } else { + result.errors.push('Password must contain at least one number'); + } + + // Check for special characters + if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) { + result.requirements.hasSpecialChars = true; + result.score += 1; + } else { + result.errors.push('Password must contain at least one special character'); + } + + // Check for common patterns + const commonPatterns = [ + /123456/, + /password/i, + /qwerty/i, + /admin/i, + /letmein/i + ]; + + for (const pattern of commonPatterns) { + if (pattern.test(password)) { + result.requirements.noCommonPatterns = false; + result.errors.push('Password contains common patterns that are easily guessable'); + break; + } + } + + // Check for repeated characters + if (/(.)\1{2,}/.test(password)) { + result.errors.push('Password should not contain repeated characters'); + } + + // Final validation based on environment + const isProduction = process.env.NODE_ENV === 'production'; + const minimumScore = isProduction ? 4 : 3; // Stricter requirements in production + + if (result.score < minimumScore) { + result.isValid = false; + if (result.errors.length === 0) { + result.errors.push('Password does not meet strength requirements'); + } + } + + return result; } /** @@ -280,52 +278,52 @@ function validatePasswordStrength(password) { * @returns {string} Generated password */ function generateRandomPassword(length = 16, options = {}) { - const defaultOptions = { - includeUppercase: true, - includeLowercase: true, - includeNumbers: true, - includeSpecialChars: true, - excludeSimilar: true, // Exclude similar looking characters - }; + const defaultOptions = { + includeUppercase: true, + includeLowercase: true, + includeNumbers: true, + includeSpecialChars: true, + excludeSimilar: true // Exclude similar looking characters + }; - const config = { ...defaultOptions, ...options }; + const config = { ...defaultOptions, ...options }; - let charset = ''; + let charset = ''; + + if (config.includeLowercase) { + charset += config.excludeSimilar ? 'abcdefghjkmnpqrstuvwxyz' : 'abcdefghijklmnopqrstuvwxyz'; + } + + if (config.includeUppercase) { + charset += config.excludeSimilar ? 'ABCDEFGHJKMNPQRSTUVWXYZ' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + } + + if (config.includeNumbers) { + charset += config.excludeSimilar ? '23456789' : '0123456789'; + } + + if (config.includeSpecialChars) { + charset += '!@#$%^&*()_+-=[]{}|;:,.<>?'; + } - if (config.includeLowercase) { - charset += config.excludeSimilar ? 'abcdefghjkmnpqrstuvwxyz' : 'abcdefghijklmnopqrstuvwxyz'; - } + if (charset === '') { + throw new Error('At least one character type must be included'); + } - if (config.includeUppercase) { - charset += config.excludeSimilar ? 'ABCDEFGHJKMNPQRSTUVWXYZ' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - } + let password = ''; + for (let i = 0; i < length; i++) { + password += charset.charAt(Math.floor(Math.random() * charset.length)); + } - if (config.includeNumbers) { - charset += config.excludeSimilar ? '23456789' : '0123456789'; - } - - if (config.includeSpecialChars) { - charset += '!@#$%^&*()_+-=[]{}|;:,.<>?'; - } - - if (charset === '') { - throw new Error('At least one character type must be included'); - } - - let password = ''; - for (let i = 0; i < length; i++) { - password += charset.charAt(Math.floor(Math.random() * charset.length)); - } - - return password; + return password; } module.exports = { - hashPassword, - verifyPassword, - needsRehash, - rehashIfNeeded, - validatePasswordStrength, - generateRandomPassword, - BCRYPT_CONFIG, -}; + hashPassword, + verifyPassword, + needsRehash, + rehashIfNeeded, + validatePasswordStrength, + generateRandomPassword, + BCRYPT_CONFIG +}; \ No newline at end of file diff --git a/src/utils/redis.js b/src/utils/redis.js index eefee60..732cb98 100644 --- a/src/utils/redis.js +++ b/src/utils/redis.js @@ -41,11 +41,6 @@ redisClient.on('reconnecting', () => { // Connect to Redis const connectRedis = async () => { - if (process.env.DISABLE_REDIS === 'true') { - logger.info('Redis connection skipped - disabled by environment variable'); - return; - } - try { await redisClient.connect(); logger.info('Connected to Redis successfully'); @@ -67,10 +62,6 @@ const redisUtils = { * @returns {Promise} Cached data or null */ get: async (key) => { - if (process.env.DISABLE_REDIS === 'true') { - return null; - } - try { const data = await redisClient.get(key); return data ? JSON.parse(data) : null; @@ -88,10 +79,6 @@ const redisUtils = { * @returns {Promise} Success status */ set: async (key, data, ttl = 3600) => { - if (process.env.DISABLE_REDIS === 'true') { - return false; - } - try { await redisClient.setEx(key, ttl, JSON.stringify(data)); return true; @@ -107,10 +94,6 @@ const redisUtils = { * @returns {Promise} Success status */ del: async (key) => { - if (process.env.DISABLE_REDIS === 'true') { - return false; - } - try { await redisClient.del(key); return true; @@ -126,10 +109,6 @@ const redisUtils = { * @returns {Promise} Exists status */ exists: async (key) => { - if (process.env.DISABLE_REDIS === 'true') { - return false; - } - try { const result = await redisClient.exists(key); return result === 1; @@ -146,10 +125,6 @@ const redisUtils = { * @returns {Promise} New value */ incr: async (key, increment = 1) => { - if (process.env.DISABLE_REDIS === 'true') { - return 0; - } - try { return await redisClient.incrBy(key, increment); } catch (error) { @@ -202,10 +177,6 @@ const redisUtils = { * @returns {Promise} Success status */ extend: async (sessionId, ttl = 86400) => { - if (process.env.DISABLE_REDIS === 'true') { - return false; - } - try { const key = `session:${sessionId}`; await redisClient.expire(key, ttl); @@ -228,11 +199,6 @@ const redisUtils = { * @returns {Promise} Success status */ publish: async (channel, data) => { - if (process.env.DISABLE_REDIS === 'true') { - logger.debug('Redis publish skipped - Redis disabled', { channel }); - return false; - } - try { await redisClient.publish(channel, JSON.stringify(data)); return true; @@ -249,15 +215,10 @@ const redisUtils = { * @returns {Promise} */ subscribe: async (channel, callback) => { - if (process.env.DISABLE_REDIS === 'true') { - logger.debug('Redis subscribe skipped - Redis disabled', { channel }); - return; - } - try { const subscriber = redisClient.duplicate(); await subscriber.connect(); - + subscriber.on('message', (receivedChannel, message) => { if (receivedChannel === channel) { try { @@ -289,21 +250,16 @@ const redisUtils = { * @returns {Promise} Rate limit status */ check: async (key, limit, window) => { - if (process.env.DISABLE_REDIS === 'true') { - // Allow all requests when Redis is disabled - return { allowed: true, count: 0, remaining: limit, resetTime: Date.now() + (window * 1000) }; - } - try { const rateLimitKey = `ratelimit:${key}`; const current = await redisClient.incr(rateLimitKey); - + if (current === 1) { await redisClient.expire(rateLimitKey, window); } - + const ttl = await redisClient.ttl(rateLimitKey); - + return { allowed: current <= limit, count: current, @@ -380,12 +336,12 @@ const redisUtils = { try { const lockKey = `lock:${key}`; const token = `${Date.now()}-${Math.random()}`; - + const result = await redisClient.set(lockKey, token, { EX: ttl, NX: true, }); - + return result === 'OK' ? token : null; } catch (error) { logger.error('Redis lock acquire error:', { key, error: error.message }); @@ -409,7 +365,7 @@ const redisUtils = { return 0 end `; - + const result = await redisClient.eval(luaScript, 1, lockKey, token); return result === 1; } catch (error) { @@ -424,14 +380,10 @@ const redisUtils = { * @returns {Promise} Health statistics */ getHealthStats: async () => { - if (process.env.DISABLE_REDIS === 'true') { - return { connected: false, disabled: true, reason: 'Redis disabled by environment variable' }; - } - try { const info = await redisClient.info(); const memory = await redisClient.info('memory'); - + return { connected: redisClient.isReady, info: info.split('\r\n').reduce((acc, line) => { @@ -452,16 +404,14 @@ const redisUtils = { }, }; -// Initialize Redis connection only if not disabled -if (process.env.NODE_ENV !== 'test' && process.env.DISABLE_REDIS !== 'true') { +// Initialize Redis connection +if (process.env.NODE_ENV !== 'test') { connectRedis().catch((error) => { logger.error('Failed to initialize Redis:', error); }); -} else if (process.env.DISABLE_REDIS === 'true') { - logger.info('Redis disabled by environment variable DISABLE_REDIS=true'); } // Attach utilities to client Object.assign(redisClient, redisUtils); -module.exports = redisClient; +module.exports = redisClient; \ No newline at end of file diff --git a/src/utils/security.js b/src/utils/security.js deleted file mode 100644 index 7a2575f..0000000 --- a/src/utils/security.js +++ /dev/null @@ -1,420 +0,0 @@ -/** - * Security Utilities - * Provides security-related helper functions for authentication and authorization - */ - -const crypto = require('crypto'); -const logger = require('./logger'); - -/** - * Generate a cryptographically secure random token - * @param {number} length - Token length in bytes (default 32) - * @returns {string} Hex-encoded random token - */ -function generateSecureToken(length = 32) { - return crypto.randomBytes(length).toString('hex'); -} - -/** - * Generate a time-limited token with embedded expiration - * @param {Object} data - Data to embed in token - * @param {number} expiresInMs - Expiration time in milliseconds - * @returns {string} Time-limited token - */ -function generateTimeLimitedToken(data, expiresInMs) { - const payload = { - data, - exp: Date.now() + expiresInMs, - nonce: crypto.randomBytes(16).toString('hex'), - }; - - const jsonPayload = JSON.stringify(payload); - const signature = crypto - .createHmac('sha256', process.env.TOKEN_SIGNING_SECRET || 'default-secret') - .update(jsonPayload) - .digest('hex'); - - const token = Buffer.from(jsonPayload).toString('base64') + '.' + signature; - return token; -} - -/** - * Verify and decode a time-limited token - * @param {string} token - Token to verify - * @returns {Object} Decoded data if valid - * @throws {Error} If token is invalid or expired - */ -function verifyTimeLimitedToken(token) { - try { - const [encodedPayload, signature] = token.split('.'); - if (!encodedPayload || !signature) { - throw new Error('Invalid token format'); - } - - const jsonPayload = Buffer.from(encodedPayload, 'base64').toString(); - const expectedSignature = crypto - .createHmac('sha256', process.env.TOKEN_SIGNING_SECRET || 'default-secret') - .update(jsonPayload) - .digest('hex'); - - if (signature !== expectedSignature) { - throw new Error('Invalid token signature'); - } - - const payload = JSON.parse(jsonPayload); - - if (Date.now() > payload.exp) { - throw new Error('Token expired'); - } - - return payload.data; - } catch (error) { - logger.warn('Token verification failed:', { error: error.message }); - throw error; - } -} - -/** - * Hash sensitive data with salt - * @param {string} data - Data to hash - * @param {string} salt - Salt to use (optional, will generate if not provided) - * @returns {Object} Hash and salt - */ -function hashWithSalt(data, salt = null) { - if (!salt) { - salt = crypto.randomBytes(32).toString('hex'); - } - - const hash = crypto - .createHmac('sha256', salt) - .update(data) - .digest('hex'); - - return { hash, salt }; -} - -/** - * Verify data against hash with salt - * @param {string} data - Data to verify - * @param {string} hash - Expected hash - * @param {string} salt - Salt used for hashing - * @returns {boolean} True if data matches hash - */ -function verifyHashWithSalt(data, hash, salt) { - const computed = crypto - .createHmac('sha256', salt) - .update(data) - .digest('hex'); - - return computed === hash; -} - -/** - * Sanitize user input to prevent injection attacks - * @param {string} input - User input to sanitize - * @param {Object} options - Sanitization options - * @returns {string} Sanitized input - */ -function sanitizeInput(input, options = {}) { - if (typeof input !== 'string') { - return input; - } - - let sanitized = input; - - // Remove null bytes - sanitized = sanitized.replace(/\0/g, ''); - - // Trim whitespace - if (options.trim !== false) { - sanitized = sanitized.trim(); - } - - // Limit length - if (options.maxLength) { - sanitized = sanitized.substring(0, options.maxLength); - } - - // Remove HTML tags if specified - if (options.stripHtml) { - sanitized = sanitized.replace(/<[^>]*>/g, ''); - } - - // Escape special characters if specified - if (options.escapeHtml) { - sanitized = sanitized - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - return sanitized; -} - -/** - * Validate email format with additional security checks - * @param {string} email - Email to validate - * @returns {Object} Validation result - */ -function validateSecureEmail(email) { - if (!email || typeof email !== 'string') { - return { isValid: false, error: 'Email is required' }; - } - - // Basic format check - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - return { isValid: false, error: 'Invalid email format' }; - } - - // Length check - if (email.length > 254) { - return { isValid: false, error: 'Email too long' }; - } - - // Check for suspicious patterns - const suspiciousPatterns = [ - /[<>]/, // HTML tags - /javascript:/i, // JavaScript protocol - /data:/i, // Data protocol - /vbscript:/i, // VBScript protocol - ]; - - for (const pattern of suspiciousPatterns) { - if (pattern.test(email)) { - return { isValid: false, error: 'Email contains invalid characters' }; - } - } - - return { isValid: true }; -} - -/** - * Rate limiting key generator - * @param {string} identifier - Base identifier (IP, user ID, etc.) - * @param {string} action - Action being rate limited - * @param {number} windowMinutes - Time window in minutes - * @returns {string} Rate limiting key - */ -function generateRateLimitKey(identifier, action, windowMinutes = 15) { - const windowStart = Math.floor(Date.now() / (windowMinutes * 60 * 1000)); - return `rate_limit:${action}:${identifier}:${windowStart}`; -} - -/** - * Generate a secure session ID - * @returns {string} Session ID - */ -function generateSessionId() { - return crypto.randomBytes(32).toString('base64url'); -} - -/** - * Validate password strength with basic length requirements only - * @param {string} password - Password to validate - * @param {Object} options - Validation options - * @returns {Object} Validation result with detailed feedback - */ -function validatePasswordStrength(password, options = {}) { - const defaults = { - minLength: 6, - maxLength: 128, - requireUppercase: false, - requireLowercase: false, - requireNumbers: false, - requireSpecialChars: false, - forbidCommonPasswords: false, - }; - - const config = { ...defaults, ...options }; - const errors = []; - const requirements = []; - - // Basic validation - password must be a string - if (!password || typeof password !== 'string') { - errors.push('Password is required'); - return { - isValid: false, - errors, - requirements: ['Valid password string'], - strength: { score: 0, level: 'invalid', feedback: 'Invalid password format', entropy: 0 }, - }; - } - - // Length checks only - if (password.length < config.minLength) { - errors.push(`Password must be at least ${config.minLength} characters long`); - } - if (password.length > config.maxLength) { - errors.push(`Password must not exceed ${config.maxLength} characters`); - } - - requirements.push(`${config.minLength}-${config.maxLength} characters`); - - // All other checks are disabled for basic validation - // This allows simple passwords like "password123" to pass - - return { - isValid: errors.length === 0, - errors, - requirements, - strength: calculatePasswordStrength(password), - }; -} - -/** - * Calculate password strength score - * @param {string} password - Password to analyze - * @returns {Object} Strength analysis - */ -function calculatePasswordStrength(password) { - let score = 0; - let feedback = []; - - // Length bonus - if (password.length >= 8) score += 1; - if (password.length >= 12) score += 1; - if (password.length >= 16) score += 1; - - // Character variety bonus - if (/[a-z]/.test(password)) score += 1; - if (/[A-Z]/.test(password)) score += 1; - if (/[0-9]/.test(password)) score += 1; - if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 1; - - // Complexity bonus - const charSetSize = - (/[a-z]/.test(password) ? 26 : 0) + - (/[A-Z]/.test(password) ? 26 : 0) + - (/[0-9]/.test(password) ? 10 : 0) + - (/[!@#$%^&*(),.?":{}|<>]/.test(password) ? 32 : 0); - - const entropy = password.length * Math.log2(charSetSize); - if (entropy >= 50) score += 1; - if (entropy >= 70) score += 1; - - // Determine strength level - let level; - if (score <= 2) { - level = 'weak'; - feedback.push('Consider using a longer password with more character types'); - } else if (score <= 4) { - level = 'fair'; - feedback.push('Good start! Add more length or character variety for better security'); - } else if (score <= 6) { - level = 'good'; - feedback.push('Strong password! Well done'); - } else { - level = 'excellent'; - feedback.push('Excellent password strength!'); - } - - return { - score: Math.min(score, 7), - level, - feedback: feedback.join(' '), - entropy: Math.round(entropy), - }; -} - -/** - * Generate CSRF token - * @param {string} sessionId - Session identifier - * @returns {string} CSRF token - */ -function generateCSRFToken(sessionId) { - const payload = { - sessionId, - timestamp: Date.now(), - nonce: crypto.randomBytes(16).toString('hex'), - }; - - const token = Buffer.from(JSON.stringify(payload)).toString('base64url'); - const signature = crypto - .createHmac('sha256', process.env.CSRF_SECRET || 'csrf-secret') - .update(token) - .digest('base64url'); - - return `${token}.${signature}`; -} - -/** - * Verify CSRF token - * @param {string} token - CSRF token to verify - * @param {string} sessionId - Expected session ID - * @param {number} maxAgeMs - Maximum token age in milliseconds - * @returns {boolean} True if token is valid - */ -function verifyCSRFToken(token, sessionId, maxAgeMs = 3600000) { - try { - const [encodedPayload, signature] = token.split('.'); - if (!encodedPayload || !signature) { - return false; - } - - const expectedSignature = crypto - .createHmac('sha256', process.env.CSRF_SECRET || 'csrf-secret') - .update(encodedPayload) - .digest('base64url'); - - if (signature !== expectedSignature) { - return false; - } - - const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString()); - - // Check session ID - if (payload.sessionId !== sessionId) { - return false; - } - - // Check token age - if (Date.now() - payload.timestamp > maxAgeMs) { - return false; - } - - return true; - } catch (error) { - logger.warn('CSRF token verification failed:', { error: error.message }); - return false; - } -} - -/** - * Audit log entry creator with security context - * @param {Object} context - Security context - * @returns {Object} Audit log entry - */ -function createSecurityAuditEntry(context) { - return { - timestamp: new Date().toISOString(), - correlationId: context.correlationId, - playerId: context.playerId, - action: context.action, - resource: context.resource, - outcome: context.outcome, // 'success', 'failure', 'blocked' - ipAddress: context.ipAddress, - userAgent: context.userAgent, - details: context.details || {}, - riskLevel: context.riskLevel || 'low', // 'low', 'medium', 'high', 'critical' - }; -} - -module.exports = { - generateSecureToken, - generateTimeLimitedToken, - verifyTimeLimitedToken, - hashWithSalt, - verifyHashWithSalt, - sanitizeInput, - validateSecureEmail, - generateRateLimitKey, - generateSessionId, - validatePasswordStrength, - calculatePasswordStrength, - generateCSRFToken, - verifyCSRFToken, - createSecurityAuditEntry, -}; \ No newline at end of file diff --git a/src/utils/validation.js b/src/utils/validation.js index 277ed39..8f6af7c 100644 --- a/src/utils/validation.js +++ b/src/utils/validation.js @@ -12,44 +12,44 @@ const logger = require('./logger'); * @returns {Object} Validation result with isValid flag and error message */ function validateEmail(email) { - const result = { isValid: false, error: null }; + const result = { isValid: false, error: null }; - if (!email || typeof email !== 'string') { - result.error = 'Email must be a non-empty string'; + if (!email || typeof email !== 'string') { + result.error = 'Email must be a non-empty string'; + return result; + } + + // Normalize email + const normalizedEmail = email.toLowerCase().trim(); + + // Check format + if (!validator.isEmail(normalizedEmail)) { + result.error = 'Invalid email format'; + return result; + } + + // Check length + if (normalizedEmail.length > 320) { // RFC 5321 limit + result.error = 'Email address too long'; + return result; + } + + // Check for common disposable email domains (optional) + const disposableDomains = [ + '10minutemail.com', + 'guerrillamail.com', + 'mailinator.com', + 'tempmail.org' + ]; + + const domain = normalizedEmail.split('@')[1]; + if (process.env.BLOCK_DISPOSABLE_EMAILS === 'true' && disposableDomains.includes(domain)) { + result.error = 'Disposable email addresses are not allowed'; + return result; + } + + result.isValid = true; return result; - } - - // Normalize email - const normalizedEmail = email.toLowerCase().trim(); - - // Check format - if (!validator.isEmail(normalizedEmail)) { - result.error = 'Invalid email format'; - return result; - } - - // Check length - if (normalizedEmail.length > 320) { // RFC 5321 limit - result.error = 'Email address too long'; - return result; - } - - // Check for common disposable email domains (optional) - const disposableDomains = [ - '10minutemail.com', - 'guerrillamail.com', - 'mailinator.com', - 'tempmail.org', - ]; - - const domain = normalizedEmail.split('@')[1]; - if (process.env.BLOCK_DISPOSABLE_EMAILS === 'true' && disposableDomains.includes(domain)) { - result.error = 'Disposable email addresses are not allowed'; - return result; - } - - result.isValid = true; - return result; } /** @@ -58,61 +58,61 @@ function validateEmail(email) { * @returns {Object} Validation result with isValid flag and error message */ function validateUsername(username) { - const result = { isValid: false, error: null }; + const result = { isValid: false, error: null }; - if (!username || typeof username !== 'string') { - result.error = 'Username must be a non-empty string'; + if (!username || typeof username !== 'string') { + result.error = 'Username must be a non-empty string'; + return result; + } + + const trimmedUsername = username.trim(); + + // Check length + const minLength = parseInt(process.env.MIN_USERNAME_LENGTH) || 3; + const maxLength = parseInt(process.env.MAX_USERNAME_LENGTH) || 20; + + if (trimmedUsername.length < minLength) { + result.error = `Username must be at least ${minLength} characters long`; + return result; + } + + if (trimmedUsername.length > maxLength) { + result.error = `Username must not exceed ${maxLength} characters`; + return result; + } + + // Check format (alphanumeric, underscores, hyphens) + if (!/^[a-zA-Z0-9_-]+$/.test(trimmedUsername)) { + result.error = 'Username can only contain letters, numbers, underscores, and hyphens'; + return result; + } + + // Must start with a letter or number + if (!/^[a-zA-Z0-9]/.test(trimmedUsername)) { + result.error = 'Username must start with a letter or number'; + return result; + } + + // Must end with a letter or number + if (!/[a-zA-Z0-9]$/.test(trimmedUsername)) { + result.error = 'Username must end with a letter or number'; + return result; + } + + // Check for reserved usernames + const reservedUsernames = [ + 'admin', 'administrator', 'root', 'system', 'api', 'www', + 'mail', 'email', 'support', 'help', 'info', 'contact', + 'null', 'undefined', 'anonymous', 'guest', 'test', 'demo' + ]; + + if (reservedUsernames.includes(trimmedUsername.toLowerCase())) { + result.error = 'Username is reserved and cannot be used'; + return result; + } + + result.isValid = true; return result; - } - - const trimmedUsername = username.trim(); - - // Check length - const minLength = parseInt(process.env.MIN_USERNAME_LENGTH) || 3; - const maxLength = parseInt(process.env.MAX_USERNAME_LENGTH) || 20; - - if (trimmedUsername.length < minLength) { - result.error = `Username must be at least ${minLength} characters long`; - return result; - } - - if (trimmedUsername.length > maxLength) { - result.error = `Username must not exceed ${maxLength} characters`; - return result; - } - - // Check format (alphanumeric, underscores, hyphens) - if (!/^[a-zA-Z0-9_-]+$/.test(trimmedUsername)) { - result.error = 'Username can only contain letters, numbers, underscores, and hyphens'; - return result; - } - - // Must start with a letter or number - if (!/^[a-zA-Z0-9]/.test(trimmedUsername)) { - result.error = 'Username must start with a letter or number'; - return result; - } - - // Must end with a letter or number - if (!/[a-zA-Z0-9]$/.test(trimmedUsername)) { - result.error = 'Username must end with a letter or number'; - return result; - } - - // Check for reserved usernames - const reservedUsernames = [ - 'admin', 'administrator', 'root', 'system', 'api', 'www', - 'mail', 'email', 'support', 'help', 'info', 'contact', - 'null', 'undefined', 'anonymous', 'guest', 'test', 'demo', - ]; - - if (reservedUsernames.includes(trimmedUsername.toLowerCase())) { - result.error = 'Username is reserved and cannot be used'; - return result; - } - - result.isValid = true; - return result; } /** @@ -121,50 +121,50 @@ function validateUsername(username) { * @returns {Object} Validation result with isValid flag and error message */ function validateCoordinates(coordinates) { - const result = { isValid: false, error: null }; + const result = { isValid: false, error: null }; - if (!coordinates || typeof coordinates !== 'string') { - result.error = 'Coordinates must be a non-empty string'; + if (!coordinates || typeof coordinates !== 'string') { + result.error = 'Coordinates must be a non-empty string'; + return result; + } + + const trimmedCoords = coordinates.trim().toUpperCase(); + + // Check format: Letter + Numbers + Hyphen + Numbers + Hyphen + Letter + const coordPattern = /^[A-Z]\d+-\d+-[A-Z]$/; + + if (!coordPattern.test(trimmedCoords)) { + result.error = 'Invalid coordinates format. Expected format: A3-91-X'; + return result; + } + + // Parse components + const parts = trimmedCoords.split('-'); + const sector = parts[0]; // A3 + const system = parts[1]; // 91 + const planet = parts[2]; // X + + // Validate sector (Letter followed by 1-2 digits) + if (!/^[A-Z]\d{1,2}$/.test(sector)) { + result.error = 'Invalid sector format in coordinates'; + return result; + } + + // Validate system (1-3 digits) + const systemNum = parseInt(system); + if (isNaN(systemNum) || systemNum < 1 || systemNum > 999) { + result.error = 'System number must be between 1 and 999'; + return result; + } + + // Validate planet (single letter) + if (!/^[A-Z]$/.test(planet)) { + result.error = 'Planet identifier must be a single letter'; + return result; + } + + result.isValid = true; return result; - } - - const trimmedCoords = coordinates.trim().toUpperCase(); - - // Check format: Letter + Numbers + Hyphen + Numbers + Hyphen + Letter - const coordPattern = /^[A-Z]\d+-\d+-[A-Z]$/; - - if (!coordPattern.test(trimmedCoords)) { - result.error = 'Invalid coordinates format. Expected format: A3-91-X'; - return result; - } - - // Parse components - const parts = trimmedCoords.split('-'); - const sector = parts[0]; // A3 - const system = parts[1]; // 91 - const planet = parts[2]; // X - - // Validate sector (Letter followed by 1-2 digits) - if (!/^[A-Z]\d{1,2}$/.test(sector)) { - result.error = 'Invalid sector format in coordinates'; - return result; - } - - // Validate system (1-3 digits) - const systemNum = parseInt(system); - if (isNaN(systemNum) || systemNum < 1 || systemNum > 999) { - result.error = 'System number must be between 1 and 999'; - return result; - } - - // Validate planet (single letter) - if (!/^[A-Z]$/.test(planet)) { - result.error = 'Planet identifier must be a single letter'; - return result; - } - - result.isValid = true; - return result; } /** @@ -174,45 +174,45 @@ function validateCoordinates(coordinates) { * @returns {Object} Validation result with isValid flag and error message */ function validateInteger(value, options = {}) { - const result = { isValid: false, error: null }; - const { min, max, fieldName = 'Value' } = options; + const result = { isValid: false, error: null }; + const { min, max, fieldName = 'Value' } = options; - // Check if value exists - if (value === null || value === undefined || value === '') { - result.error = `${fieldName} is required`; + // Check if value exists + if (value === null || value === undefined || value === '') { + result.error = `${fieldName} is required`; + return result; + } + + // Convert to number + const numValue = Number(value); + + // Check if it's a valid number + if (isNaN(numValue)) { + result.error = `${fieldName} must be a valid number`; + return result; + } + + // Check if it's an integer + if (!Number.isInteger(numValue)) { + result.error = `${fieldName} must be an integer`; + return result; + } + + // Check minimum value + if (min !== undefined && numValue < min) { + result.error = `${fieldName} must be at least ${min}`; + return result; + } + + // Check maximum value + if (max !== undefined && numValue > max) { + result.error = `${fieldName} must not exceed ${max}`; + return result; + } + + result.isValid = true; + result.value = numValue; return result; - } - - // Convert to number - const numValue = Number(value); - - // Check if it's a valid number - if (isNaN(numValue)) { - result.error = `${fieldName} must be a valid number`; - return result; - } - - // Check if it's an integer - if (!Number.isInteger(numValue)) { - result.error = `${fieldName} must be an integer`; - return result; - } - - // Check minimum value - if (min !== undefined && numValue < min) { - result.error = `${fieldName} must be at least ${min}`; - return result; - } - - // Check maximum value - if (max !== undefined && numValue > max) { - result.error = `${fieldName} must not exceed ${max}`; - return result; - } - - result.isValid = true; - result.value = numValue; - return result; } /** @@ -222,64 +222,64 @@ function validateInteger(value, options = {}) { * @returns {Object} Validation result with isValid flag and error message */ function validateString(value, options = {}) { - const result = { isValid: false, error: null }; - const { - minLength = 0, - maxLength = 1000, - fieldName = 'Value', - required = true, - trim = true, - allowEmpty = false, - } = options; + const result = { isValid: false, error: null }; + const { + minLength = 0, + maxLength = 1000, + fieldName = 'Value', + required = true, + trim = true, + allowEmpty = false + } = options; - // Check if value exists - if (value === null || value === undefined) { - if (required) { - result.error = `${fieldName} is required`; - return result; - } else { - result.isValid = true; - result.value = null; - return result; + // Check if value exists + if (value === null || value === undefined) { + if (required) { + result.error = `${fieldName} is required`; + return result; + } else { + result.isValid = true; + result.value = null; + return result; + } } - } - // Check if it's a string - if (typeof value !== 'string') { - result.error = `${fieldName} must be a string`; - return result; - } - - // Trim if requested - const processedValue = trim ? value.trim() : value; - - // Check for empty string - if (!allowEmpty && processedValue === '') { - if (required) { - result.error = `${fieldName} cannot be empty`; - return result; - } else { - result.isValid = true; - result.value = processedValue; - return result; + // Check if it's a string + if (typeof value !== 'string') { + result.error = `${fieldName} must be a string`; + return result; } - } - // Check minimum length - if (processedValue.length < minLength) { - result.error = `${fieldName} must be at least ${minLength} characters long`; + // Trim if requested + const processedValue = trim ? value.trim() : value; + + // Check for empty string + if (!allowEmpty && processedValue === '') { + if (required) { + result.error = `${fieldName} cannot be empty`; + return result; + } else { + result.isValid = true; + result.value = processedValue; + return result; + } + } + + // Check minimum length + if (processedValue.length < minLength) { + result.error = `${fieldName} must be at least ${minLength} characters long`; + return result; + } + + // Check maximum length + if (processedValue.length > maxLength) { + result.error = `${fieldName} must not exceed ${maxLength} characters`; + return result; + } + + result.isValid = true; + result.value = processedValue; return result; - } - - // Check maximum length - if (processedValue.length > maxLength) { - result.error = `${fieldName} must not exceed ${maxLength} characters`; - return result; - } - - result.isValid = true; - result.value = processedValue; - return result; } /** @@ -289,59 +289,59 @@ function validateString(value, options = {}) { * @returns {Object} Validation result with isValid flag and error message */ function validateArray(value, options = {}) { - const result = { isValid: false, error: null }; - const { - minLength = 0, - maxLength = 100, - fieldName = 'Array', - required = true, - itemValidator = null, - } = options; + const result = { isValid: false, error: null }; + const { + minLength = 0, + maxLength = 100, + fieldName = 'Array', + required = true, + itemValidator = null + } = options; - // Check if value exists - if (value === null || value === undefined) { - if (required) { - result.error = `${fieldName} is required`; - return result; - } else { - result.isValid = true; - result.value = []; - return result; + // Check if value exists + if (value === null || value === undefined) { + if (required) { + result.error = `${fieldName} is required`; + return result; + } else { + result.isValid = true; + result.value = []; + return result; + } } - } - // Check if it's an array - if (!Array.isArray(value)) { - result.error = `${fieldName} must be an array`; - return result; - } - - // Check minimum length - if (value.length < minLength) { - result.error = `${fieldName} must contain at least ${minLength} items`; - return result; - } - - // Check maximum length - if (value.length > maxLength) { - result.error = `${fieldName} must not contain more than ${maxLength} items`; - return result; - } - - // Validate individual items if validator provided - if (itemValidator && typeof itemValidator === 'function') { - for (let i = 0; i < value.length; i++) { - const itemResult = itemValidator(value[i], i); - if (!itemResult.isValid) { - result.error = `${fieldName}[${i}]: ${itemResult.error}`; + // Check if it's an array + if (!Array.isArray(value)) { + result.error = `${fieldName} must be an array`; return result; - } } - } - result.isValid = true; - result.value = value; - return result; + // Check minimum length + if (value.length < minLength) { + result.error = `${fieldName} must contain at least ${minLength} items`; + return result; + } + + // Check maximum length + if (value.length > maxLength) { + result.error = `${fieldName} must not contain more than ${maxLength} items`; + return result; + } + + // Validate individual items if validator provided + if (itemValidator && typeof itemValidator === 'function') { + for (let i = 0; i < value.length; i++) { + const itemResult = itemValidator(value[i], i); + if (!itemResult.isValid) { + result.error = `${fieldName}[${i}]: ${itemResult.error}`; + return result; + } + } + } + + result.isValid = true; + result.value = value; + return result; } /** @@ -351,40 +351,40 @@ function validateArray(value, options = {}) { * @returns {Object} Validation result with isValid flag and error message */ function validateUUID(uuid, options = {}) { - const result = { isValid: false, error: null }; - const { fieldName = 'UUID', required = true, version = null } = options; + const result = { isValid: false, error: null }; + const { fieldName = 'UUID', required = true, version = null } = options; - // Check if value exists - if (!uuid) { - if (required) { - result.error = `${fieldName} is required`; - return result; - } else { - result.isValid = true; - result.value = null; - return result; + // Check if value exists + if (!uuid) { + if (required) { + result.error = `${fieldName} is required`; + return result; + } else { + result.isValid = true; + result.value = null; + return result; + } } - } - // Check if it's a string - if (typeof uuid !== 'string') { - result.error = `${fieldName} must be a string`; + // Check if it's a string + if (typeof uuid !== 'string') { + result.error = `${fieldName} must be a string`; + return result; + } + + // Validate UUID format + const uuidValidation = version ? + validator.isUUID(uuid, version) : + validator.isUUID(uuid); + + if (!uuidValidation) { + result.error = `Invalid ${fieldName} format`; + return result; + } + + result.isValid = true; + result.value = uuid; return result; - } - - // Validate UUID format - const uuidValidation = version ? - validator.isUUID(uuid, version) : - validator.isUUID(uuid); - - if (!uuidValidation) { - result.error = `Invalid ${fieldName} format`; - return result; - } - - result.isValid = true; - result.value = uuid; - return result; } /** @@ -393,18 +393,18 @@ function validateUUID(uuid, options = {}) { * @returns {string} Sanitized HTML */ function sanitizeHTML(html) { - if (!html || typeof html !== 'string') { - return ''; - } + if (!html || typeof html !== 'string') { + return ''; + } - // Basic HTML sanitization - remove script tags and dangerous attributes - return html - .replace(/)<[^<]*)*<\/script>/gi, '') - .replace(/on\w+="[^"]*"/gi, '') - .replace(/on\w+='[^']*'/gi, '') - .replace(/javascript:/gi, '') - .replace(/vbscript:/gi, '') - .replace(/data:/gi, ''); + // Basic HTML sanitization - remove script tags and dangerous attributes + return html + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/on\w+="[^"]*"/gi, '') + .replace(/on\w+='[^']*'/gi, '') + .replace(/javascript:/gi, '') + .replace(/vbscript:/gi, '') + .replace(/data:/gi, ''); } /** @@ -414,19 +414,19 @@ function sanitizeHTML(html) { * @returns {string} Rate limiting key */ function generateRateLimitKey(req, action) { - const ip = req.ip || req.connection.remoteAddress; - const userId = req.user?.playerId || req.user?.adminId || 'anonymous'; - return `ratelimit:${action}:${userId}:${ip}`; + const ip = req.ip || req.connection.remoteAddress; + const userId = req.user?.playerId || req.user?.adminId || 'anonymous'; + return `ratelimit:${action}:${userId}:${ip}`; } module.exports = { - validateEmail, - validateUsername, - validateCoordinates, - validateInteger, - validateString, - validateArray, - validateUUID, - sanitizeHTML, - generateRateLimitKey, -}; + validateEmail, + validateUsername, + validateCoordinates, + validateInteger, + validateString, + validateArray, + validateUUID, + sanitizeHTML, + generateRateLimitKey +}; \ No newline at end of file diff --git a/src/utils/websocket.js b/src/utils/websocket.js index 50f33e8..433c37c 100644 --- a/src/utils/websocket.js +++ b/src/utils/websocket.js @@ -15,14 +15,14 @@ function initializeWebSocketHandlers(io) { io.use(async (socket, next) => { try { const token = socket.handshake.auth.token; - + if (!token) { return next(new Error('Authentication token required')); } // Verify JWT token const decoded = jwt.verify(token, process.env.JWT_SECRET); - + // Get user from database const tableName = decoded.type === 'admin' ? 'admin_users' : 'players'; const user = await db(tableName) @@ -45,7 +45,7 @@ function initializeWebSocketHandlers(io) { // Attach user info to socket socket.user = user; socket.userType = decoded.type; - + next(); } catch (error) { logger.warn('WebSocket authentication failed', { @@ -61,7 +61,7 @@ function initializeWebSocketHandlers(io) { try { // Store connection in database await storeConnection(socket); - + logger.info('WebSocket connection established', { socketId: socket.id, userId: socket.user.id, @@ -168,9 +168,9 @@ function setupPlayerEventHandlers(socket, io) { logger.debug('Game action received', { playerId: socket.user.id, action: data.action, - data, + data: data, }); - + if (callback) callback({ success: true }); } catch (error) { logger.error('Game action error:', error); @@ -218,7 +218,7 @@ function setupAdminEventHandlers(socket, io) { adminId: socket.user.id, command: data.command, }); - + if (callback) callback({ success: true }); } catch (error) { logger.error('Admin system error:', error); @@ -411,7 +411,7 @@ async function handleDisconnection(socket, reason) { */ function broadcastEvent(io, eventType, data, scopeType = null, scopeId = null) { const roomName = getRoomName(eventType, scopeType, scopeId); - + io.to(roomName).emit(eventType, { type: eventType, data, @@ -428,4 +428,4 @@ function broadcastEvent(io, eventType, data, scopeType = null, scopeId = null) { module.exports = { initializeWebSocketHandlers, broadcastEvent, -}; +}; \ No newline at end of file diff --git a/src/validators/auth.validators.js b/src/validators/auth.validators.js deleted file mode 100644 index fc7f166..0000000 --- a/src/validators/auth.validators.js +++ /dev/null @@ -1,429 +0,0 @@ -/** - * Enhanced Authentication Validators - * Comprehensive validation schemas for authentication-related endpoints - */ - -const Joi = require('joi'); -const { validateSecureEmail, validatePasswordStrength } = require('../utils/security'); - -/** - * Custom email validation with security checks - */ -const secureEmailValidator = (value, helpers) => { - const validation = validateSecureEmail(value); - if (!validation.isValid) { - return helpers.error('string.email', { message: validation.error }); - } - return value.toLowerCase().trim(); -}; - -/** - * Custom password validation with strength requirements - */ -const securePasswordValidator = (value, helpers) => { - const validation = validatePasswordStrength(value); - if (!validation.isValid) { - return helpers.error('string.password', { - message: 'Password does not meet security requirements', - details: { - errors: validation.errors, - requirements: validation.requirements, - strength: validation.strength, - } - }); - } - return value; -}; - -/** - * Username validation with security considerations - */ -const usernameValidator = Joi.string() - .min(3) - .max(30) - .pattern(/^[a-zA-Z0-9_-]+$/) - .required() - .messages({ - 'string.pattern.base': 'Username can only contain letters, numbers, underscores, and hyphens', - 'string.min': 'Username must be at least 3 characters long', - 'string.max': 'Username cannot exceed 30 characters', - 'any.required': 'Username is required', - }); - -/** - * Token validation (for verification and reset tokens) - */ -const tokenValidator = Joi.string() - .length(64) - .hex() - .required() - .messages({ - 'string.length': 'Invalid token format', - 'string.hex': 'Invalid token format', - 'any.required': 'Token is required', - }); - -/** - * Player registration validation schema (simplified for development) - */ -const registerPlayerSchema = Joi.object({ - email: Joi.string() - .email() - .required() - .messages({ - 'string.email': 'Please provide a valid email address', - 'any.required': 'Email is required', - }), - - username: usernameValidator, - - password: Joi.string() - .min(6) - .max(128) - .required() - .messages({ - 'string.min': 'Password must be at least 6 characters long', - 'string.max': 'Password cannot exceed 128 characters', - 'any.required': 'Password is required', - }), - - // Optional terms acceptance - acceptTerms: Joi.boolean() - .valid(true) - .messages({ - 'any.only': 'You must accept the terms of service', - }), -}); - -/** - * Player login validation schema (simplified for development) - */ -const loginPlayerSchema = Joi.object({ - email: Joi.string() - .email() - .required() - .messages({ - 'string.email': 'Please provide a valid email address', - 'any.required': 'Email is required', - }), - - password: Joi.string() - .min(1) - .required() - .messages({ - 'string.min': 'Password is required', - 'any.required': 'Password is required', - }), - - // Optional remember me flag - rememberMe: Joi.boolean() - .default(false), -}); - -/** - * Email verification validation schema - */ -const verifyEmailSchema = Joi.object({ - token: tokenValidator, -}); - -/** - * Resend email verification validation schema (simplified for development) - */ -const resendVerificationSchema = Joi.object({ - email: Joi.string() - .email() - .required() - .messages({ - 'string.email': 'Please provide a valid email address', - 'any.required': 'Email is required', - }), -}); - -/** - * Password reset request validation schema (simplified for development) - */ -const requestPasswordResetSchema = Joi.object({ - email: Joi.string() - .email() - .required() - .messages({ - 'string.email': 'Please provide a valid email address', - 'any.required': 'Email is required', - }), -}); - -/** - * Password reset validation schema (simplified for development) - */ -const resetPasswordSchema = Joi.object({ - token: tokenValidator, - - newPassword: Joi.string() - .min(6) - .max(128) - .required() - .messages({ - 'string.min': 'New password must be at least 6 characters long', - 'string.max': 'New password cannot exceed 128 characters', - 'any.required': 'New password is required', - }), - - confirmPassword: Joi.string() - .valid(Joi.ref('newPassword')) - .required() - .messages({ - 'any.only': 'Password confirmation does not match', - 'any.required': 'Password confirmation is required', - }), -}); - -/** - * Change password validation schema (simplified for development) - */ -const changePasswordSchema = Joi.object({ - currentPassword: Joi.string() - .min(1) - .required() - .messages({ - 'string.min': 'Current password is required', - 'any.required': 'Current password is required', - }), - - newPassword: Joi.string() - .min(6) - .max(128) - .required() - .messages({ - 'string.min': 'New password must be at least 6 characters long', - 'string.max': 'New password cannot exceed 128 characters', - 'any.required': 'New password is required', - }), - - confirmPassword: Joi.string() - .valid(Joi.ref('newPassword')) - .required() - .messages({ - 'any.only': 'Password confirmation does not match', - 'any.required': 'Password confirmation is required', - }), -}); - -/** - * Refresh token validation schema - */ -const refreshTokenSchema = Joi.object({ - refreshToken: Joi.string() - .required() - .messages({ - 'any.required': 'Refresh token is required', - }), -}); - -/** - * Profile update validation schema - */ -const updateProfileSchema = Joi.object({ - username: usernameValidator.optional(), - - // Add other updatable fields as needed - displayName: Joi.string() - .min(1) - .max(50) - .optional() - .messages({ - 'string.min': 'Display name cannot be empty', - 'string.max': 'Display name cannot exceed 50 characters', - }), - - bio: Joi.string() - .max(500) - .optional() - .messages({ - 'string.max': 'Bio cannot exceed 500 characters', - }), -}).min(1).messages({ - 'object.min': 'At least one field must be provided for update', -}); - -/** - * Security preferences validation schema - */ -const securityPreferencesSchema = Joi.object({ - twoFactorEnabled: Joi.boolean() - .optional(), - - securityNotifications: Joi.boolean() - .optional(), - - loginNotifications: Joi.boolean() - .optional(), -}); - -/** - * Validation middleware factory - * @param {Object} schema - Joi validation schema - * @param {string} source - Source of data to validate ('body', 'query', 'params') - */ -function validateRequest(schema, source = 'body') { - return (req, res, next) => { - const correlationId = req.correlationId; - const data = req[source]; - - const { error, value } = schema.validate(data, { - abortEarly: false, - stripUnknown: true, - convert: true, - }); - - if (error) { - const errors = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - type: detail.type, - context: detail.context, - })); - - logger.warn('Validation failed', { - correlationId, - source, - errors, - path: req.path, - method: req.method, - }); - - return res.status(400).json({ - success: false, - message: 'Validation failed', - code: 'VALIDATION_ERROR', - correlationId, - errors, - }); - } - - // Replace request data with validated/sanitized data - req[source] = value; - next(); - }; -} - -/** - * Complex validation for registration that checks uniqueness - * This middleware should be used after basic validation - */ -function validateRegistrationUniqueness() { - return async (req, res, next) => { - try { - const correlationId = req.correlationId; - const { email, username } = req.body; - const db = require('../database/connection'); - const errors = []; - - // Check email uniqueness - const existingEmail = await db('players') - .where('email', email.toLowerCase()) - .first(); - - if (existingEmail) { - errors.push({ - field: 'email', - message: 'Email address is already registered', - type: 'unique', - }); - } - - // Check username uniqueness - const existingUsername = await db('players') - .where('username', username) - .first(); - - if (existingUsername) { - errors.push({ - field: 'username', - message: 'Username is already taken', - type: 'unique', - }); - } - - if (errors.length > 0) { - logger.warn('Registration uniqueness validation failed', { - correlationId, - errors, - email, - username, - }); - - return res.status(409).json({ - success: false, - message: 'Registration validation failed', - code: 'UNIQUENESS_ERROR', - correlationId, - errors, - }); - } - - next(); - - } catch (error) { - logger.error('Registration uniqueness validation error', { - correlationId: req.correlationId, - error: error.message, - }); - - return res.status(500).json({ - success: false, - message: 'Internal server error', - correlationId: req.correlationId, - }); - } - }; -} - -/** - * Rate limiting validation for sensitive operations - */ -function validateRateLimit(action, maxRequests = 5, windowMinutes = 15) { - return (req, res, next) => { - const { rateLimiter } = require('../middleware/security.middleware'); - - const rateLimitMiddleware = rateLimiter({ - maxRequests, - windowMinutes, - action, - keyGenerator: (req) => { - // Use email for auth-related actions, IP for others - return req.body.email || req.user?.email || req.ip || 'unknown'; - }, - }); - - return rateLimitMiddleware(req, res, next); - }; -} - -// Import logger -const logger = require('../utils/logger'); - -module.exports = { - // Validation schemas - registerPlayerSchema, - loginPlayerSchema, - verifyEmailSchema, - resendVerificationSchema, - requestPasswordResetSchema, - resetPasswordSchema, - changePasswordSchema, - refreshTokenSchema, - updateProfileSchema, - securityPreferencesSchema, - - // Validation middleware - validateRequest, - validateRegistrationUniqueness, - validateRateLimit, - - // Individual validators for reuse - secureEmailValidator, - securePasswordValidator, - usernameValidator, - tokenValidator, -}; \ No newline at end of file diff --git a/src/validators/colony.validators.js b/src/validators/colony.validators.js deleted file mode 100644 index a37942e..0000000 --- a/src/validators/colony.validators.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Colony Validation Schemas - * Joi validation schemas for colony-related API endpoints - */ - -const Joi = require('joi'); - -// Colony creation validation -const createColonySchema = Joi.object({ - name: Joi.string() - .min(3) - .max(50) - .pattern(/^[a-zA-Z0-9\s\-_]+$/) - .required() - .messages({ - 'string.min': 'Colony name must be at least 3 characters long', - 'string.max': 'Colony name cannot exceed 50 characters', - 'string.pattern.base': 'Colony name can only contain letters, numbers, spaces, hyphens, and underscores', - 'any.required': 'Colony name is required', - }), - - coordinates: Joi.string() - .pattern(/^[A-Z]\d+-\d+-[A-Z]$/) - .required() - .messages({ - 'string.pattern.base': 'Coordinates must be in the format: A3-91-X (Letter+Number-Number-Letter)', - 'any.required': 'Coordinates are required', - }), - - planet_type_id: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Planet type ID must be a number', - 'number.integer': 'Planet type ID must be an integer', - 'number.min': 'Planet type ID must be at least 1', - 'any.required': 'Planet type ID is required', - }), -}); - -// Building construction validation -const constructBuildingSchema = Joi.object({ - building_type_id: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Building type ID must be a number', - 'number.integer': 'Building type ID must be an integer', - 'number.min': 'Building type ID must be at least 1', - 'any.required': 'Building type ID is required', - }), -}); - -// Colony ID parameter validation -const colonyIdParamSchema = Joi.object({ - colonyId: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Colony ID must be a number', - 'number.integer': 'Colony ID must be an integer', - 'number.min': 'Colony ID must be at least 1', - 'any.required': 'Colony ID is required', - }), -}); - -// Building upgrade validation (for future use) -const upgradeBuildingSchema = Joi.object({ - building_id: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Building ID must be a number', - 'number.integer': 'Building ID must be an integer', - 'number.min': 'Building ID must be at least 1', - 'any.required': 'Building ID is required', - }), - - target_level: Joi.number() - .integer() - .min(2) - .max(20) - .optional() - .messages({ - 'number.base': 'Target level must be a number', - 'number.integer': 'Target level must be an integer', - 'number.min': 'Target level must be at least 2', - 'number.max': 'Target level cannot exceed 20', - }), -}); - -module.exports = { - createColonySchema, - constructBuildingSchema, - colonyIdParamSchema, - upgradeBuildingSchema, -}; diff --git a/src/validators/combat.validators.js b/src/validators/combat.validators.js deleted file mode 100644 index e87d283..0000000 --- a/src/validators/combat.validators.js +++ /dev/null @@ -1,324 +0,0 @@ -/** - * Combat Validation Schemas - * Joi validation schemas for combat-related endpoints - */ - -const Joi = require('joi'); - -// Combat initiation validation schema -const initiateCombatSchema = Joi.object({ - attacker_fleet_id: Joi.number().integer().positive().required() - .messages({ - 'number.base': 'Attacker fleet ID must be a number', - 'number.integer': 'Attacker fleet ID must be an integer', - 'number.positive': 'Attacker fleet ID must be positive', - 'any.required': 'Attacker fleet ID is required', - }), - - defender_fleet_id: Joi.number().integer().positive().allow(null) - .messages({ - 'number.base': 'Defender fleet ID must be a number', - 'number.integer': 'Defender fleet ID must be an integer', - 'number.positive': 'Defender fleet ID must be positive', - }), - - defender_colony_id: Joi.number().integer().positive().allow(null) - .messages({ - 'number.base': 'Defender colony ID must be a number', - 'number.integer': 'Defender colony ID must be an integer', - 'number.positive': 'Defender colony ID must be positive', - }), - - location: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required() - .messages({ - 'string.pattern.base': 'Location must be in format A3-91-X', - 'any.required': 'Location is required', - }), - - combat_type: Joi.string().valid('instant', 'turn_based', 'tactical', 'real_time').default('instant') - .messages({ - 'any.only': 'Combat type must be one of: instant, turn_based, tactical, real_time', - }), - - tactical_settings: Joi.object({ - formation: Joi.string().valid('standard', 'defensive', 'aggressive', 'flanking', 'escort').optional(), - priority_targets: Joi.array().items(Joi.string()).optional(), - engagement_range: Joi.string().valid('close', 'medium', 'long').optional(), - retreat_threshold: Joi.number().min(0).max(100).optional(), - }).optional(), -}).custom((value, helpers) => { - // Ensure exactly one defender is specified - const hasFleetDefender = value.defender_fleet_id !== null && value.defender_fleet_id !== undefined; - const hasColonyDefender = value.defender_colony_id !== null && value.defender_colony_id !== undefined; - - if (hasFleetDefender && hasColonyDefender) { - return helpers.error('custom.bothDefenders'); - } - - if (!hasFleetDefender && !hasColonyDefender) { - return helpers.error('custom.noDefender'); - } - - return value; -}, 'defender validation').messages({ - 'custom.bothDefenders': 'Cannot specify both defender fleet and defender colony', - 'custom.noDefender': 'Must specify either defender fleet or defender colony', -}); - -// Fleet position update validation schema -const updateFleetPositionSchema = Joi.object({ - position_x: Joi.number().min(-1000).max(1000).default(0) - .messages({ - 'number.base': 'Position X must be a number', - 'number.min': 'Position X must be at least -1000', - 'number.max': 'Position X must be at most 1000', - }), - - position_y: Joi.number().min(-1000).max(1000).default(0) - .messages({ - 'number.base': 'Position Y must be a number', - 'number.min': 'Position Y must be at least -1000', - 'number.max': 'Position Y must be at most 1000', - }), - - position_z: Joi.number().min(-1000).max(1000).default(0) - .messages({ - 'number.base': 'Position Z must be a number', - 'number.min': 'Position Z must be at least -1000', - 'number.max': 'Position Z must be at most 1000', - }), - - formation: Joi.string().valid('standard', 'defensive', 'aggressive', 'flanking', 'escort').default('standard') - .messages({ - 'any.only': 'Formation must be one of: standard, defensive, aggressive, flanking, escort', - }), - - tactical_settings: Joi.object({ - auto_engage: Joi.boolean().default(true), - engagement_range: Joi.string().valid('close', 'medium', 'long').default('medium'), - target_priority: Joi.string().valid('closest', 'weakest', 'strongest', 'random').default('closest'), - retreat_threshold: Joi.number().min(0).max(100).default(25), - formation_spacing: Joi.number().min(0.1).max(10.0).default(1.0), - }).default({}), -}); - -// Combat history query parameters validation schema -const combatHistoryQuerySchema = Joi.object({ - limit: Joi.number().integer().min(1).max(100).default(20) - .messages({ - 'number.base': 'Limit must be a number', - 'number.integer': 'Limit must be an integer', - 'number.min': 'Limit must be at least 1', - 'number.max': 'Limit cannot exceed 100', - }), - - offset: Joi.number().integer().min(0).default(0) - .messages({ - 'number.base': 'Offset must be a number', - 'number.integer': 'Offset must be an integer', - 'number.min': 'Offset must be at least 0', - }), - - outcome: Joi.string().valid('attacker_victory', 'defender_victory', 'draw').optional() - .messages({ - 'any.only': 'Outcome must be one of: attacker_victory, defender_victory, draw', - }), - - battle_type: Joi.string().valid('fleet_vs_fleet', 'fleet_vs_colony', 'siege').optional() - .messages({ - 'any.only': 'Battle type must be one of: fleet_vs_fleet, fleet_vs_colony, siege', - }), - - start_date: Joi.date().iso().optional() - .messages({ - 'date.format': 'Start date must be in ISO format', - }), - - end_date: Joi.date().iso().optional() - .messages({ - 'date.format': 'End date must be in ISO format', - }), -}).custom((value, helpers) => { - // Ensure end_date is after start_date if both are provided - if (value.start_date && value.end_date && value.end_date <= value.start_date) { - return helpers.error('custom.invalidDateRange'); - } - return value; -}, 'date validation').messages({ - 'custom.invalidDateRange': 'End date must be after start date', -}); - -// Combat queue query parameters validation schema -const combatQueueQuerySchema = Joi.object({ - status: Joi.string().valid('pending', 'processing', 'completed', 'failed').optional() - .messages({ - 'any.only': 'Status must be one of: pending, processing, completed, failed', - }), - - limit: Joi.number().integer().min(1).max(100).default(50) - .messages({ - 'number.base': 'Limit must be a number', - 'number.integer': 'Limit must be an integer', - 'number.min': 'Limit must be at least 1', - 'number.max': 'Limit cannot exceed 100', - }), - - priority_min: Joi.number().integer().min(1).max(1000).optional() - .messages({ - 'number.base': 'Priority minimum must be a number', - 'number.integer': 'Priority minimum must be an integer', - 'number.min': 'Priority minimum must be at least 1', - 'number.max': 'Priority minimum cannot exceed 1000', - }), - - priority_max: Joi.number().integer().min(1).max(1000).optional() - .messages({ - 'number.base': 'Priority maximum must be a number', - 'number.integer': 'Priority maximum must be an integer', - 'number.min': 'Priority maximum must be at least 1', - 'number.max': 'Priority maximum cannot exceed 1000', - }), -}).custom((value, helpers) => { - // Ensure priority_max is greater than priority_min if both are provided - if (value.priority_min && value.priority_max && value.priority_max <= value.priority_min) { - return helpers.error('custom.invalidPriorityRange'); - } - return value; -}, 'priority validation').messages({ - 'custom.invalidPriorityRange': 'Priority maximum must be greater than priority minimum', -}); - -// Parameter validation schemas -const battleIdParamSchema = Joi.object({ - battleId: Joi.number().integer().positive().required() - .messages({ - 'number.base': 'Battle ID must be a number', - 'number.integer': 'Battle ID must be an integer', - 'number.positive': 'Battle ID must be positive', - 'any.required': 'Battle ID is required', - }), -}); - -const fleetIdParamSchema = Joi.object({ - fleetId: Joi.number().integer().positive().required() - .messages({ - 'number.base': 'Fleet ID must be a number', - 'number.integer': 'Fleet ID must be an integer', - 'number.positive': 'Fleet ID must be positive', - 'any.required': 'Fleet ID is required', - }), -}); - -const encounterIdParamSchema = Joi.object({ - encounterId: Joi.number().integer().positive().required() - .messages({ - 'number.base': 'Encounter ID must be a number', - 'number.integer': 'Encounter ID must be an integer', - 'number.positive': 'Encounter ID must be positive', - 'any.required': 'Encounter ID is required', - }), -}); - -// Combat configuration validation schema (admin only) -const combatConfigurationSchema = Joi.object({ - config_name: Joi.string().min(3).max(100).required() - .messages({ - 'string.min': 'Configuration name must be at least 3 characters', - 'string.max': 'Configuration name cannot exceed 100 characters', - 'any.required': 'Configuration name is required', - }), - - combat_type: Joi.string().valid('instant', 'turn_based', 'tactical', 'real_time').required() - .messages({ - 'any.only': 'Combat type must be one of: instant, turn_based, tactical, real_time', - 'any.required': 'Combat type is required', - }), - - config_data: Joi.object({ - auto_resolve: Joi.boolean().default(true), - preparation_time: Joi.number().integer().min(0).max(300).default(30), - max_rounds: Joi.number().integer().min(1).max(100).default(20), - round_duration: Joi.number().integer().min(1).max(60).default(5), - damage_variance: Joi.number().min(0).max(1).default(0.1), - experience_gain: Joi.number().min(0).max(10).default(1.0), - casualty_rate_min: Joi.number().min(0).max(1).default(0.1), - casualty_rate_max: Joi.number().min(0).max(1).default(0.8), - loot_multiplier: Joi.number().min(0).max(10).default(1.0), - spectator_limit: Joi.number().integer().min(0).max(1000).default(100), - priority: Joi.number().integer().min(1).max(1000).default(100), - }).required() - .custom((value, helpers) => { - // Ensure casualty_rate_max >= casualty_rate_min - if (value.casualty_rate_max < value.casualty_rate_min) { - return helpers.error('custom.invalidCasualtyRange'); - } - return value; - }, 'casualty rate validation') - .messages({ - 'custom.invalidCasualtyRange': 'Maximum casualty rate must be greater than or equal to minimum casualty rate', - }), - - description: Joi.string().max(500).optional() - .messages({ - 'string.max': 'Description cannot exceed 500 characters', - }), - - is_active: Joi.boolean().default(true), -}); - -// Export validation functions -const validateInitiateCombat = (data) => { - return initiateCombatSchema.validate(data, { abortEarly: false }); -}; - -const validateUpdateFleetPosition = (data) => { - return updateFleetPositionSchema.validate(data, { abortEarly: false }); -}; - -const validateCombatHistoryQuery = (data) => { - return combatHistoryQuerySchema.validate(data, { abortEarly: false }); -}; - -const validateCombatQueueQuery = (data) => { - return combatQueueQuerySchema.validate(data, { abortEarly: false }); -}; - -const validateBattleIdParam = (data) => { - return battleIdParamSchema.validate(data, { abortEarly: false }); -}; - -const validateFleetIdParam = (data) => { - return fleetIdParamSchema.validate(data, { abortEarly: false }); -}; - -const validateEncounterIdParam = (data) => { - return encounterIdParamSchema.validate(data, { abortEarly: false }); -}; - -const validateCombatConfiguration = (data) => { - return combatConfigurationSchema.validate(data, { abortEarly: false }); -}; - -module.exports = { - // Validation functions - validateInitiateCombat, - validateUpdateFleetPosition, - validateCombatHistoryQuery, - validateCombatQueueQuery, - validateBattleIdParam, - validateFleetIdParam, - validateEncounterIdParam, - validateCombatConfiguration, - - // Raw schemas for middleware use - schemas: { - initiateCombat: initiateCombatSchema, - updateFleetPosition: updateFleetPositionSchema, - combatHistoryQuery: combatHistoryQuerySchema, - combatQueueQuery: combatQueueQuerySchema, - battleIdParam: battleIdParamSchema, - fleetIdParam: fleetIdParamSchema, - encounterIdParam: encounterIdParamSchema, - combatConfiguration: combatConfigurationSchema, - }, -}; diff --git a/src/validators/fleet.validators.js b/src/validators/fleet.validators.js deleted file mode 100644 index 949a49b..0000000 --- a/src/validators/fleet.validators.js +++ /dev/null @@ -1,401 +0,0 @@ -/** - * Fleet Validation Schemas - * Input validation for fleet operations using Joi - */ - -const Joi = require('joi'); -const logger = require('../utils/logger'); - -/** - * Fleet creation validation schema - */ -const createFleetSchema = Joi.object({ - name: Joi.string() - .min(3) - .max(50) - .pattern(/^[a-zA-Z0-9\s\-_]+$/) - .required() - .messages({ - 'string.min': 'Fleet name must be at least 3 characters long', - 'string.max': 'Fleet name must not exceed 50 characters', - 'string.pattern.base': 'Fleet name can only contain letters, numbers, spaces, hyphens, and underscores', - 'any.required': 'Fleet name is required' - }), - - location: Joi.string() - .pattern(/^[A-Z]\d+-\d+-[A-Z]$/) - .required() - .messages({ - 'string.pattern.base': 'Location must be valid coordinates (e.g., A3-91-X)', - 'any.required': 'Fleet location is required' - }), - - ship_composition: Joi.array() - .items( - Joi.object({ - design_id: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Ship design ID must be a number', - 'number.integer': 'Ship design ID must be an integer', - 'number.min': 'Ship design ID must be at least 1', - 'any.required': 'Ship design ID is required' - }), - - quantity: Joi.number() - .integer() - .min(1) - .max(100) - .required() - .messages({ - 'number.base': 'Ship quantity must be a number', - 'number.integer': 'Ship quantity must be an integer', - 'number.min': 'Ship quantity must be at least 1', - 'number.max': 'Ship quantity cannot exceed 100 per design', - 'any.required': 'Ship quantity is required' - }) - }) - ) - .min(1) - .max(10) - .required() - .messages({ - 'array.min': 'Fleet must contain at least one ship design', - 'array.max': 'Fleet cannot contain more than 10 different ship designs', - 'any.required': 'Ship composition is required' - }) -}); - -/** - * Fleet movement validation schema - */ -const moveFleetSchema = Joi.object({ - destination: Joi.string() - .pattern(/^[A-Z]\d+-\d+-[A-Z]$/) - .required() - .messages({ - 'string.pattern.base': 'Destination must be valid coordinates (e.g., A3-91-X)', - 'any.required': 'Destination is required' - }) -}); - -/** - * Fleet ID parameter validation - */ -const fleetIdSchema = Joi.object({ - fleetId: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Fleet ID must be a number', - 'number.integer': 'Fleet ID must be an integer', - 'number.min': 'Fleet ID must be at least 1', - 'any.required': 'Fleet ID is required' - }) -}); - -/** - * Ship design query validation - */ -const shipDesignQuerySchema = Joi.object({ - ship_class: Joi.string() - .valid('fighter', 'corvette', 'frigate', 'destroyer', 'cruiser', 'battleship', 'carrier', 'support') - .optional() - .messages({ - 'any.only': 'Ship class must be one of: fighter, corvette, frigate, destroyer, cruiser, battleship, carrier, support' - }), - - tier: Joi.number() - .integer() - .min(1) - .max(5) - .optional() - .messages({ - 'number.base': 'Tier must be a number', - 'number.integer': 'Tier must be an integer', - 'number.min': 'Tier must be at least 1', - 'number.max': 'Tier must not exceed 5' - }), - - available_only: Joi.boolean() - .optional() - .default(true) - .messages({ - 'boolean.base': 'Available only must be a boolean value' - }) -}); - -/** - * Ship design ID parameter validation - */ -const designIdSchema = Joi.object({ - designId: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Design ID must be a number', - 'number.integer': 'Design ID must be an integer', - 'number.min': 'Design ID must be at least 1', - 'any.required': 'Design ID is required' - }) -}); - -/** - * Fleet status update validation - */ -const updateFleetStatusSchema = Joi.object({ - status: Joi.string() - .valid('idle', 'moving', 'in_combat', 'constructing', 'repairing') - .required() - .messages({ - 'any.only': 'Status must be one of: idle, moving, in_combat, constructing, repairing', - 'any.required': 'Status is required' - }) -}); - -/** - * Fleet reinforcement validation - */ -const reinforceFleetSchema = Joi.object({ - ship_composition: Joi.array() - .items( - Joi.object({ - design_id: Joi.number() - .integer() - .min(1) - .required(), - - quantity: Joi.number() - .integer() - .min(1) - .max(50) - .required() - }) - ) - .min(1) - .max(5) - .required() - .messages({ - 'array.min': 'Reinforcement must contain at least one ship design', - 'array.max': 'Reinforcement cannot contain more than 5 different ship designs', - 'any.required': 'Ship composition is required' - }) -}); - -/** - * Fleet repair validation - */ -const repairFleetSchema = Joi.object({ - repair_all: Joi.boolean() - .optional() - .default(true), - - ship_ids: Joi.array() - .items( - Joi.number() - .integer() - .min(1) - ) - .when('repair_all', { - is: false, - then: Joi.required(), - otherwise: Joi.optional() - }) - .messages({ - 'array.base': 'Ship IDs must be an array', - 'any.required': 'Ship IDs are required when repair_all is false' - }) -}); - -/** - * Pagination validation schema - */ -const paginationSchema = Joi.object({ - page: Joi.number() - .integer() - .min(1) - .optional() - .default(1) - .messages({ - 'number.base': 'Page must be a number', - 'number.integer': 'Page must be an integer', - 'number.min': 'Page must be at least 1' - }), - - limit: Joi.number() - .integer() - .min(1) - .max(100) - .optional() - .default(20) - .messages({ - 'number.base': 'Limit must be a number', - 'number.integer': 'Limit must be an integer', - 'number.min': 'Limit must be at least 1', - 'number.max': 'Limit cannot exceed 100' - }), - - sort_by: Joi.string() - .valid('name', 'created_at', 'total_ships', 'fleet_status', 'location') - .optional() - .default('created_at') - .messages({ - 'any.only': 'Sort by must be one of: name, created_at, total_ships, fleet_status, location' - }), - - sort_order: Joi.string() - .valid('asc', 'desc') - .optional() - .default('desc') - .messages({ - 'any.only': 'Sort order must be either asc or desc' - }) -}); - -/** - * Validation middleware factory - * @param {Joi.Schema} schema - Joi validation schema - * @param {string} source - Source of data to validate ('body', 'params', 'query') - * @returns {Function} Express middleware function - */ -function validateRequest(schema, source = 'body') { - return (req, res, next) => { - const data = req[source]; - const { error, value } = schema.validate(data, { - abortEarly: false, - stripUnknown: true, - convert: true - }); - - if (error) { - const errors = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - type: detail.type - })); - - logger.warn('Fleet validation failed', { - correlationId: req.correlationId, - source, - errors, - originalData: data - }); - - return res.status(400).json({ - error: 'Validation failed', - details: errors, - message: 'Please check your input and try again' - }); - } - - // Replace request data with validated and sanitized data - req[source] = value; - next(); - }; -} - -/** - * Custom validation functions - */ -const customValidations = { - /** - * Validate fleet ownership - * @param {number} fleetId - Fleet ID - * @param {number} playerId - Player ID - * @returns {Promise} True if player owns the fleet - */ - async validateFleetOwnership(fleetId, playerId) { - try { - const db = require('../database/connection'); - const fleet = await db('fleets') - .select('id') - .where('id', fleetId) - .where('player_id', playerId) - .first(); - - return !!fleet; - } catch (error) { - return false; - } - }, - - /** - * Validate colony ownership - * @param {string} coordinates - Colony coordinates - * @param {number} playerId - Player ID - * @returns {Promise} True if player owns the colony - */ - async validateColonyOwnership(coordinates, playerId) { - try { - const db = require('../database/connection'); - const colony = await db('colonies') - .select('id') - .where('coordinates', coordinates) - .where('player_id', playerId) - .first(); - - return !!colony; - } catch (error) { - return false; - } - }, - - /** - * Validate fleet can perform action - * @param {number} fleetId - Fleet ID - * @param {string} requiredStatus - Required fleet status - * @returns {Promise} True if fleet can perform the action - */ - async validateFleetAction(fleetId, requiredStatus = 'idle') { - try { - const db = require('../database/connection'); - const fleet = await db('fleets') - .select('fleet_status') - .where('id', fleetId) - .first(); - - if (!fleet) return false; - - if (Array.isArray(requiredStatus)) { - return requiredStatus.includes(fleet.fleet_status); - } - - return fleet.fleet_status === requiredStatus; - } catch (error) { - return false; - } - } -}; - -module.exports = { - // Validation schemas - createFleetSchema, - moveFleetSchema, - fleetIdSchema, - shipDesignQuerySchema, - designIdSchema, - updateFleetStatusSchema, - reinforceFleetSchema, - repairFleetSchema, - paginationSchema, - - // Middleware factory - validateRequest, - - // Custom validations - customValidations, - - // Convenience middleware functions - validateCreateFleet: validateRequest(createFleetSchema, 'body'), - validateMoveFleet: validateRequest(moveFleetSchema, 'body'), - validateFleetId: validateRequest(fleetIdSchema, 'params'), - validateDesignId: validateRequest(designIdSchema, 'params'), - validateShipDesignQuery: validateRequest(shipDesignQuerySchema, 'query'), - validatePagination: validateRequest(paginationSchema, 'query'), - validateReinforceFleet: validateRequest(reinforceFleetSchema, 'body'), - validateRepairFleet: validateRequest(repairFleetSchema, 'body') -}; \ No newline at end of file diff --git a/src/validators/research.validators.js b/src/validators/research.validators.js deleted file mode 100644 index 21ad6ce..0000000 --- a/src/validators/research.validators.js +++ /dev/null @@ -1,353 +0,0 @@ -/** - * Research Validation Schemas - * Joi validation schemas for research-related API requests - */ - -const Joi = require('joi'); - -/** - * Schema for starting research - */ -const startResearchSchema = Joi.object({ - technology_id: Joi.number() - .integer() - .min(1) - .max(1000) - .required() - .messages({ - 'number.base': 'Technology ID must be a number', - 'number.integer': 'Technology ID must be an integer', - 'number.min': 'Technology ID must be at least 1', - 'number.max': 'Technology ID must be at most 1000', - 'any.required': 'Technology ID is required' - }) -}).options({ - abortEarly: false, - stripUnknown: true -}); - -/** - * Schema for research facility creation - */ -const createResearchFacilitySchema = Joi.object({ - colony_id: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Colony ID must be a number', - 'number.integer': 'Colony ID must be an integer', - 'number.min': 'Colony ID must be at least 1', - 'any.required': 'Colony ID is required' - }), - - name: Joi.string() - .min(3) - .max(100) - .pattern(/^[a-zA-Z0-9\s\-_.]+$/) - .required() - .messages({ - 'string.min': 'Facility name must be at least 3 characters long', - 'string.max': 'Facility name must be at most 100 characters long', - 'string.pattern.base': 'Facility name can only contain letters, numbers, spaces, hyphens, underscores, and periods', - 'any.required': 'Facility name is required' - }), - - facility_type: Joi.string() - .valid('research_lab', 'advanced_lab', 'quantum_lab', 'specialized_lab') - .required() - .messages({ - 'any.only': 'Facility type must be one of: research_lab, advanced_lab, quantum_lab, specialized_lab', - 'any.required': 'Facility type is required' - }), - - specialization: Joi.array() - .items( - Joi.string().valid('military', 'industrial', 'social', 'exploration') - ) - .max(2) - .optional() - .messages({ - 'array.max': 'Maximum 2 specializations allowed', - 'any.only': 'Specialization must be one of: military, industrial, social, exploration' - }) -}).options({ - abortEarly: false, - stripUnknown: true -}); - -/** - * Schema for research facility updates - */ -const updateResearchFacilitySchema = Joi.object({ - name: Joi.string() - .min(3) - .max(100) - .pattern(/^[a-zA-Z0-9\s\-_.]+$/) - .optional() - .messages({ - 'string.min': 'Facility name must be at least 3 characters long', - 'string.max': 'Facility name must be at most 100 characters long', - 'string.pattern.base': 'Facility name can only contain letters, numbers, spaces, hyphens, underscores, and periods' - }), - - is_active: Joi.boolean() - .optional() - .messages({ - 'boolean.base': 'Active status must be true or false' - }), - - specialization: Joi.array() - .items( - Joi.string().valid('military', 'industrial', 'social', 'exploration') - ) - .max(2) - .optional() - .messages({ - 'array.max': 'Maximum 2 specializations allowed', - 'any.only': 'Specialization must be one of: military, industrial, social, exploration' - }) -}).options({ - abortEarly: false, - stripUnknown: true -}); - -/** - * Schema for technology tree filtering - */ -const technologyTreeFilterSchema = Joi.object({ - category: Joi.string() - .valid('military', 'industrial', 'social', 'exploration') - .optional() - .messages({ - 'any.only': 'Category must be one of: military, industrial, social, exploration' - }), - - tier: Joi.number() - .integer() - .min(1) - .max(5) - .optional() - .messages({ - 'number.integer': 'Tier must be an integer', - 'number.min': 'Tier must be at least 1', - 'number.max': 'Tier must be at most 5' - }), - - status: Joi.string() - .valid('unavailable', 'available', 'researching', 'completed') - .optional() - .messages({ - 'any.only': 'Status must be one of: unavailable, available, researching, completed' - }), - - include_unavailable: Joi.boolean() - .optional() - .default(false) - .messages({ - 'boolean.base': 'Include unavailable must be true or false' - }), - - sort_by: Joi.string() - .valid('tier', 'category', 'name', 'research_time', 'status') - .optional() - .default('tier') - .messages({ - 'any.only': 'Sort by must be one of: tier, category, name, research_time, status' - }), - - sort_order: Joi.string() - .valid('asc', 'desc') - .optional() - .default('asc') - .messages({ - 'any.only': 'Sort order must be asc or desc' - }) -}).options({ - abortEarly: false, - stripUnknown: true -}); - -/** - * Schema for research queue operations - */ -const researchQueueSchema = Joi.object({ - action: Joi.string() - .valid('add', 'remove', 'reorder', 'clear') - .required() - .messages({ - 'any.only': 'Action must be one of: add, remove, reorder, clear', - 'any.required': 'Action is required' - }), - - technology_id: Joi.number() - .integer() - .min(1) - .when('action', { - is: Joi.valid('add', 'remove'), - then: Joi.required(), - otherwise: Joi.optional() - }) - .messages({ - 'number.base': 'Technology ID must be a number', - 'number.integer': 'Technology ID must be an integer', - 'number.min': 'Technology ID must be at least 1', - 'any.required': 'Technology ID is required for add/remove actions' - }), - - position: Joi.number() - .integer() - .min(1) - .max(10) - .when('action', { - is: 'reorder', - then: Joi.required(), - otherwise: Joi.optional() - }) - .messages({ - 'number.integer': 'Position must be an integer', - 'number.min': 'Position must be at least 1', - 'number.max': 'Position must be at most 10', - 'any.required': 'Position is required for reorder action' - }) -}).options({ - abortEarly: false, - stripUnknown: true -}); - -/** - * Schema for research statistics queries - */ -const researchStatsSchema = Joi.object({ - timeframe: Joi.string() - .valid('day', 'week', 'month', 'year', 'all') - .optional() - .default('month') - .messages({ - 'any.only': 'Timeframe must be one of: day, week, month, year, all' - }), - - category: Joi.string() - .valid('military', 'industrial', 'social', 'exploration') - .optional() - .messages({ - 'any.only': 'Category must be one of: military, industrial, social, exploration' - }), - - include_details: Joi.boolean() - .optional() - .default(false) - .messages({ - 'boolean.base': 'Include details must be true or false' - }) -}).options({ - abortEarly: false, - stripUnknown: true -}); - -/** - * Validation middleware factory - * @param {Object} schema - Joi schema to validate against - * @param {string} source - Request property to validate ('body', 'query', 'params') - * @returns {Function} Express middleware function - */ -function validateRequest(schema, source = 'body') { - return (req, res, next) => { - const data = req[source]; - const { error, value } = schema.validate(data); - - if (error) { - const validationErrors = error.details.map(detail => ({ - field: detail.path.join('.'), - message: detail.message, - value: detail.context?.value - })); - - return res.status(400).json({ - success: false, - error: 'Validation failed', - details: { - validation_errors: validationErrors - }, - correlationId: req.correlationId - }); - } - - // Replace the original data with the validated and sanitized data - req[source] = value; - next(); - }; -} - -/** - * Validation middleware for technology ID parameter - */ -const validateTechnologyId = (req, res, next) => { - const technologyId = parseInt(req.params.technologyId || req.params.id); - - if (!technologyId || !Number.isInteger(technologyId) || technologyId < 1) { - return res.status(400).json({ - success: false, - error: 'Invalid technology ID', - details: { - validation_errors: [{ - field: 'technologyId', - message: 'Technology ID must be a positive integer', - value: req.params.technologyId || req.params.id - }] - }, - correlationId: req.correlationId - }); - } - - req.technologyId = technologyId; - next(); -}; - -/** - * Validation middleware for research facility ID parameter - */ -const validateFacilityId = (req, res, next) => { - const facilityId = parseInt(req.params.facilityId || req.params.id); - - if (!facilityId || !Number.isInteger(facilityId) || facilityId < 1) { - return res.status(400).json({ - success: false, - error: 'Invalid facility ID', - details: { - validation_errors: [{ - field: 'facilityId', - message: 'Facility ID must be a positive integer', - value: req.params.facilityId || req.params.id - }] - }, - correlationId: req.correlationId - }); - } - - req.facilityId = facilityId; - next(); -}; - -module.exports = { - // Schemas - startResearchSchema, - createResearchFacilitySchema, - updateResearchFacilitySchema, - technologyTreeFilterSchema, - researchQueueSchema, - researchStatsSchema, - - // Middleware functions - validateRequest, - validateTechnologyId, - validateFacilityId, - - // Specific validation middleware - validateStartResearch: validateRequest(startResearchSchema, 'body'), - validateCreateFacility: validateRequest(createResearchFacilitySchema, 'body'), - validateUpdateFacility: validateRequest(updateResearchFacilitySchema, 'body'), - validateTechnologyTreeFilter: validateRequest(technologyTreeFilterSchema, 'query'), - validateResearchQueue: validateRequest(researchQueueSchema, 'body'), - validateResearchStats: validateRequest(researchStatsSchema, 'query') -}; \ No newline at end of file diff --git a/src/validators/resource.validators.js b/src/validators/resource.validators.js deleted file mode 100644 index c15a240..0000000 --- a/src/validators/resource.validators.js +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Resource Validation Schemas - * Joi validation schemas for resource-related API endpoints - */ - -const Joi = require('joi'); - -// Resource transfer validation -const transferResourcesSchema = Joi.object({ - fromColonyId: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Source colony ID must be a number', - 'number.integer': 'Source colony ID must be an integer', - 'number.min': 'Source colony ID must be at least 1', - 'any.required': 'Source colony ID is required', - }), - - toColonyId: Joi.number() - .integer() - .min(1) - .required() - .messages({ - 'number.base': 'Destination colony ID must be a number', - 'number.integer': 'Destination colony ID must be an integer', - 'number.min': 'Destination colony ID must be at least 1', - 'any.required': 'Destination colony ID is required', - }), - - resources: Joi.object() - .pattern( - Joi.string().valid('scrap', 'energy', 'data_cores', 'rare_elements'), - Joi.number().integer().min(1).max(1000000), - ) - .min(1) - .required() - .custom((value, helpers) => { - if (Object.keys(value).length === 0) { - return helpers.error('any.required'); - } - return value; - }) - .messages({ - 'object.min': 'At least one resource must be specified', - 'any.required': 'Resources object is required and must contain at least one resource', - }), -}); - -// Add resources validation (for development/testing) -const addResourcesSchema = Joi.object({ - resources: Joi.object() - .pattern( - Joi.string().valid('scrap', 'energy', 'data_cores', 'rare_elements'), - Joi.number().integer().min(1).max(1000000), - ) - .min(1) - .required() - .custom((value, helpers) => { - if (Object.keys(value).length === 0) { - return helpers.error('any.required'); - } - return value; - }) - .messages({ - 'object.min': 'At least one resource must be specified', - 'any.required': 'Resources object is required and must contain at least one resource', - }), -}); - -// Resource amount validation helper -const resourceAmountSchema = Joi.number() - .integer() - .min(0) - .max(Number.MAX_SAFE_INTEGER) - .messages({ - 'number.base': 'Resource amount must be a number', - 'number.integer': 'Resource amount must be an integer', - 'number.min': 'Resource amount cannot be negative', - 'number.max': 'Resource amount is too large', - }); - -// Resource type validation -const resourceTypeSchema = Joi.string() - .valid('scrap', 'energy', 'data_cores', 'rare_elements') - .messages({ - 'any.only': 'Invalid resource type. Valid types are: scrap, energy, data_cores, rare_elements', - }); - -// Query parameters for resource endpoints -const resourceQuerySchema = Joi.object({ - includeProduction: Joi.boolean() - .optional() - .default(false) - .messages({ - 'boolean.base': 'includeProduction must be a boolean value', - }), - - includeColonyBreakdown: Joi.boolean() - .optional() - .default(false) - .messages({ - 'boolean.base': 'includeColonyBreakdown must be a boolean value', - }), - - format: Joi.string() - .valid('detailed', 'summary') - .optional() - .default('summary') - .messages({ - 'any.only': 'Format must be either "detailed" or "summary"', - }), -}); - -module.exports = { - transferResourcesSchema, - addResourcesSchema, - resourceAmountSchema, - resourceTypeSchema, - resourceQuerySchema, -}; diff --git a/start-game.js b/start-game.js deleted file mode 100644 index 9f60528..0000000 --- a/start-game.js +++ /dev/null @@ -1,725 +0,0 @@ -#!/usr/bin/env node - -/** - * Shattered Void MMO - Comprehensive Startup Orchestrator - * - * This script provides a complete startup solution for the Shattered Void MMO, - * handling all aspects of system initialization, validation, and monitoring. - * - * Features: - * - Pre-flight system checks - * - Database connectivity and migration validation - * - Redis connectivity with fallback handling - * - Backend and frontend server startup - * - Health monitoring and service validation - * - Graceful error handling and recovery - * - Performance metrics and logging - */ - -const path = require('path'); -const { spawn, exec } = require('child_process'); -const fs = require('fs').promises; -const http = require('http'); -const express = require('express'); - -// Load environment variables -require('dotenv').config(); - -// Import our custom modules -const StartupChecks = require('./scripts/startup-checks'); -const HealthMonitor = require('./scripts/health-monitor'); -const DatabaseValidator = require('./scripts/database-validator'); - -// Node.js version compatibility checking -function getNodeVersion() { - const version = process.version; - const match = version.match(/^v(\d+)\.(\d+)\.(\d+)/); - if (!match) { - throw new Error(`Unable to parse Node.js version: ${version}`); - } - return { - major: parseInt(match[1], 10), - minor: parseInt(match[2], 10), - patch: parseInt(match[3], 10), - full: version - }; -} - -function isViteCompatible() { - const nodeVersion = getNodeVersion(); - // Vite 7.x requires Node.js 20+ for crypto.hash() support - return nodeVersion.major >= 20; -} - -// Configuration -const config = { - backend: { - port: process.env.PORT || 3000, - host: process.env.HOST || '0.0.0.0', - script: 'src/server.js', - startupTimeout: 30000 - }, - frontend: { - port: process.env.FRONTEND_PORT || 5173, - host: process.env.FRONTEND_HOST || '0.0.0.0', - directory: './frontend', - buildDirectory: './frontend/dist', - startupTimeout: 20000 - }, - database: { - checkTimeout: 10000, - migrationTimeout: 30000 - }, - redis: { - checkTimeout: 5000, - optional: true - }, - startup: { - mode: process.env.NODE_ENV || 'development', - enableFrontend: process.env.ENABLE_FRONTEND !== 'false', - enableHealthMonitoring: process.env.ENABLE_HEALTH_MONITORING !== 'false', - healthCheckInterval: 30000, - maxRetries: 3, - retryDelay: 2000, - frontendFallback: process.env.FRONTEND_FALLBACK !== 'false' - } -}; - -// Color codes for console output -const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m', - white: '\x1b[37m' -}; - -// Process tracking -const processes = { - backend: null, - frontend: null -}; - -// Startup state -const startupState = { - startTime: Date.now(), - phase: 'initialization', - services: {}, - metrics: {} -}; - -/** - * Enhanced logging with colors and timestamps - */ -function log(level, message, data = null) { - const timestamp = new Date().toISOString(); - const pid = process.pid; - - let colorCode = colors.white; - let prefix = 'INFO'; - - switch (level) { - case 'error': - colorCode = colors.red; - prefix = 'ERROR'; - break; - case 'warn': - colorCode = colors.yellow; - prefix = 'WARN'; - break; - case 'success': - colorCode = colors.green; - prefix = 'SUCCESS'; - break; - case 'info': - colorCode = colors.cyan; - prefix = 'INFO'; - break; - case 'debug': - colorCode = colors.magenta; - prefix = 'DEBUG'; - break; - } - - const logMessage = `${colors.bright}[${timestamp}] [PID:${pid}] [${prefix}]${colors.reset} ${colorCode}${message}${colors.reset}`; - console.log(logMessage); - - if (data) { - console.log(`${colors.blue}${JSON.stringify(data, null, 2)}${colors.reset}`); - } -} - -/** - * Display startup banner - */ -function displayBanner() { - const banner = ` -${colors.cyan}╔═══════════════════════════════════════════════════════════════╗ -║ ║ -║ ${colors.bright}SHATTERED VOID MMO STARTUP${colors.reset}${colors.cyan} ║ -║ ${colors.white}Post-Collapse Galaxy Strategy Game${colors.reset}${colors.cyan} ║ -║ ║ -║ ${colors.yellow}Mode:${colors.reset} ${colors.white}${config.startup.mode.toUpperCase()}${colors.reset}${colors.cyan} ║ -║ ${colors.yellow}Backend:${colors.reset} ${colors.white}${config.backend.host}:${config.backend.port}${colors.reset}${colors.cyan} ║ -║ ${colors.yellow}Frontend:${colors.reset} ${colors.white}${config.startup.enableFrontend ? `${config.frontend.host}:${config.frontend.port}` : 'Disabled'}${colors.reset}${colors.cyan} ║ -║ ║ -╚═══════════════════════════════════════════════════════════════╝${colors.reset} -`; - console.log(banner); -} - -/** - * Update startup phase - */ -function updatePhase(phase, details = null) { - startupState.phase = phase; - log('info', `Starting phase: ${phase}`, details); -} - -/** - * Measure execution time - */ -function measureTime(startTime) { - return Date.now() - startTime; -} - -/** - * Check if a port is available - */ -function checkPort(port, host = 'localhost') { - return new Promise((resolve) => { - const server = require('net').createServer(); - - server.listen(port, host, () => { - server.once('close', () => resolve(true)); - server.close(); - }); - - server.on('error', () => resolve(false)); - }); -} - -/** - * Wait for a service to become available - */ -function waitForService(host, port, timeout = 10000, retries = 10) { - return new Promise((resolve, reject) => { - let attempts = 0; - const interval = timeout / retries; - - const check = () => { - attempts++; - - const req = http.request({ - hostname: host, - port: port, - path: '/health', - method: 'GET', - timeout: 2000 - }, (res) => { - if (res.statusCode === 200) { - resolve(true); - } else if (attempts < retries) { - setTimeout(check, interval); - } else { - reject(new Error(`Service not ready after ${attempts} attempts`)); - } - }); - - req.on('error', () => { - if (attempts < retries) { - setTimeout(check, interval); - } else { - reject(new Error(`Service not reachable after ${attempts} attempts`)); - } - }); - - req.end(); - }; - - check(); - }); -} - -/** - * Spawn a process with enhanced monitoring - */ -function spawnProcess(command, args, options = {}) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - stdio: ['pipe', 'pipe', 'pipe'], - ...options - }); - - child.stdout.on('data', (data) => { - const output = data.toString().trim(); - if (output) { - log('debug', `[${command}] ${output}`); - } - }); - - child.stderr.on('data', (data) => { - const output = data.toString().trim(); - if (output && !output.includes('ExperimentalWarning')) { - log('warn', `[${command}] ${output}`); - } - }); - - child.on('error', (error) => { - log('error', `Process error for ${command}:`, error); - reject(error); - }); - - child.on('exit', (code, signal) => { - if (code !== 0 && signal !== 'SIGTERM') { - const error = new Error(`Process ${command} exited with code ${code}`); - log('error', error.message); - reject(error); - } - }); - - // Consider the process started if it doesn't exit within a second - setTimeout(() => { - if (!child.killed) { - resolve(child); - } - }, 1000); - }); -} - -/** - * Pre-flight system checks - */ -async function runPreflightChecks() { - updatePhase('Pre-flight Checks'); - const startTime = Date.now(); - - try { - const checks = new StartupChecks(); - const results = await checks.runAllChecks(); - - const duration = measureTime(startTime); - startupState.metrics.preflightDuration = duration; - - if (results.success) { - log('success', `Pre-flight checks completed in ${duration}ms`); - startupState.services.preflight = { status: 'healthy', checks: results.checks }; - } else { - log('error', 'Pre-flight checks failed:', results.failures); - throw new Error('Pre-flight validation failed'); - } - } catch (error) { - log('error', 'Pre-flight checks error:', error); - throw error; - } -} - -/** - * Validate database connectivity and run migrations - */ -async function validateDatabase() { - updatePhase('Database Validation'); - const startTime = Date.now(); - - try { - const validator = new DatabaseValidator(); - const results = await validator.validateDatabase(); - - const duration = measureTime(startTime); - startupState.metrics.databaseDuration = duration; - - if (results.success) { - log('success', `Database validation completed in ${duration}ms`); - startupState.services.database = { status: 'healthy', ...results }; - } else { - // Detailed error logging for database validation failures - const errorDetails = { - general: results.error, - connectivity: results.connectivity?.error || null, - migrations: results.migrations?.error || null, - schema: results.schema?.error || null, - missingTables: results.schema?.missingTables || [], - seeds: results.seeds?.error || null, - integrity: results.integrity?.error || null - }; - - log("error", "Database validation failed:", errorDetails); - - if (results.schema && !results.schema.success) { - log("error", `Schema validation failed - Missing tables: ${results.schema.missingTables.join(", ")}`); - log("info", `Current coverage: ${results.schema.coverage}`); - if (results.schema.troubleshooting) { - log("info", "Troubleshooting suggestions:"); - results.schema.troubleshooting.forEach(tip => log("info", ` - ${tip}`)); - } - } - - throw new Error(`Database validation failed: ${JSON.stringify(errorDetails, null, 2)}`); - } - } catch (error) { - log('error', 'Database validation error:', error); - throw error; - } -} - -/** - * Start the backend server - */ -async function startBackendServer() { - updatePhase('Backend Server Startup'); - const startTime = Date.now(); - - try { - // Check if port is available - const portAvailable = await checkPort(config.backend.port, config.backend.host); - if (!portAvailable) { - throw new Error(`Backend port ${config.backend.port} is already in use`); - } - - // Start the backend process - log('info', `Starting backend server on ${config.backend.host}:${config.backend.port}`); - const backendProcess = await spawnProcess('node', [config.backend.script], { - env: { ...process.env, NODE_ENV: config.startup.mode } - }); - - processes.backend = backendProcess; - - // Wait for the server to be ready - await waitForService(config.backend.host, config.backend.port, config.backend.startupTimeout); - - const duration = measureTime(startTime); - startupState.metrics.backendDuration = duration; - - log('success', `Backend server started in ${duration}ms`); - startupState.services.backend = { - status: 'healthy', - port: config.backend.port, - pid: backendProcess.pid - }; - } catch (error) { - log('error', 'Backend server startup failed:', error); - throw error; - } -} - -/** - * Serve built frontend using Express static server - */ -async function serveBuildFrontend() { - log('info', 'Starting built frontend static server...'); - - try { - // Check if built frontend exists - await fs.access(config.frontend.buildDirectory); - - // Create Express app for serving static files - const app = express(); - - // Serve static files from build directory - app.use(express.static(config.frontend.buildDirectory)); - - // Handle SPA routing - serve index.html for all non-file requests - app.get('*', (req, res) => { - res.sendFile(path.join(process.cwd(), config.frontend.buildDirectory, 'index.html')); - }); - - // Start the static server - const server = app.listen(config.frontend.port, config.frontend.host, () => { - log('success', `Built frontend served on ${config.frontend.host}:${config.frontend.port}`); - }); - - // Store server reference for cleanup - processes.frontend = { - kill: (signal) => { - server.close(); - }, - pid: process.pid - }; - - return server; - } catch (error) { - log('error', 'Failed to serve built frontend:', error); - throw error; - } -} - -/** - * Build and start the frontend server - */ -async function startFrontendServer() { - if (!config.startup.enableFrontend) { - log('info', 'Frontend disabled by configuration'); - return; - } - - updatePhase('Frontend Server Startup'); - const startTime = Date.now(); - - // Check Node.js version compatibility with Vite - const nodeVersion = getNodeVersion(); - const viteCompatible = isViteCompatible(); - - log('info', `Node.js version: ${nodeVersion.full}`); - - if (!viteCompatible) { - log('warn', `Node.js ${nodeVersion.full} is not compatible with Vite 7.x (requires Node.js 20+)`); - log('warn', 'crypto.hash() function is not available in this Node.js version'); - - if (config.startup.frontendFallback) { - log('info', 'Attempting to serve built frontend as fallback...'); - try { - await serveBuildFrontend(); - - const duration = measureTime(startTime); - startupState.metrics.frontendDuration = duration; - - log('success', `Built frontend fallback started in ${duration}ms`); - startupState.services.frontend = { - status: 'healthy', - port: config.frontend.port, - mode: 'static', - nodeCompatibility: 'fallback' - }; - return; - } catch (fallbackError) { - log('error', 'Frontend fallback also failed:', fallbackError); - throw new Error(`Both Vite dev server and static fallback failed: ${fallbackError.message}`); - } - } else { - throw new Error(`Node.js ${nodeVersion.full} is incompatible with Vite 7.x. Upgrade to Node.js 20+ or enable fallback mode.`); - } - } - - try { - // Check if frontend directory exists - await fs.access(config.frontend.directory); - - // Check if port is available - const portAvailable = await checkPort(config.frontend.port, config.frontend.host); - if (!portAvailable) { - throw new Error(`Frontend port ${config.frontend.port} is already in use`); - } - - log('info', `Starting Vite development server on ${config.frontend.host}:${config.frontend.port}`); - - // Start the frontend development server - const frontendProcess = await spawnProcess('npm', ['run', 'dev'], { - cwd: config.frontend.directory, - env: { - ...process.env, - PORT: config.frontend.port, - HOST: config.frontend.host - } - }); - - processes.frontend = frontendProcess; - - // Wait for the server to be ready - await waitForService(config.frontend.host, config.frontend.port, config.frontend.startupTimeout); - - const duration = measureTime(startTime); - startupState.metrics.frontendDuration = duration; - - log('success', `Vite development server started in ${duration}ms`); - startupState.services.frontend = { - status: 'healthy', - port: config.frontend.port, - pid: frontendProcess.pid, - mode: 'development', - nodeCompatibility: 'compatible' - }; - } catch (error) { - log('error', 'Vite development server startup failed:', error); - - // Try fallback to built frontend if enabled and we haven't tried it yet - if (config.startup.frontendFallback && viteCompatible) { - log('warn', 'Attempting to serve built frontend as fallback...'); - try { - await serveBuildFrontend(); - - const duration = measureTime(startTime); - startupState.metrics.frontendDuration = duration; - - log('success', `Built frontend fallback started in ${duration}ms`); - startupState.services.frontend = { - status: 'healthy', - port: config.frontend.port, - mode: 'static', - nodeCompatibility: 'fallback' - }; - return; - } catch (fallbackError) { - log('error', 'Frontend fallback also failed:', fallbackError); - } - } - - // Frontend failure is not critical if we're running in production mode - if (config.startup.mode === 'production') { - log('warn', 'Continuing without frontend in production mode'); - } else { - throw error; - } - } -} - -/** - * Start health monitoring - */ -async function startHealthMonitoring() { - if (!config.startup.enableHealthMonitoring) { - log('info', 'Health monitoring disabled by configuration'); - return; - } - - updatePhase('Health Monitoring Initialization'); - - try { - const monitor = new HealthMonitor({ - services: startupState.services, - interval: config.startup.healthCheckInterval, - onHealthChange: (service, status) => { - log(status === 'healthy' ? 'success' : 'error', - `Service ${service} status: ${status}`); - } - }); - - await monitor.start(); - - log('success', 'Health monitoring started'); - startupState.services.healthMonitor = { status: 'healthy' }; - } catch (error) { - log('error', 'Health monitoring startup failed:', error); - // Health monitoring failure is not critical - } -} - -/** - * Display startup summary - */ -function displayStartupSummary() { - const totalDuration = measureTime(startupState.startTime); - - log('success', `🚀 Shattered Void MMO startup completed in ${totalDuration}ms`); - - const summary = ` -${colors.green}╔═══════════════════════════════════════════════════════════════╗ -║ STARTUP SUMMARY ║ -╠═══════════════════════════════════════════════════════════════╣${colors.reset} -${colors.white}║ Total Duration: ${totalDuration}ms${' '.repeat(47 - totalDuration.toString().length)}║ -║ ║${colors.reset} -${colors.cyan}║ Services Status: ║${colors.reset}`; - - console.log(summary); - - Object.entries(startupState.services).forEach(([service, info]) => { - const status = info.status === 'healthy' ? '✅' : '❌'; - const serviceName = service.charAt(0).toUpperCase() + service.slice(1); - const port = info.port ? `:${info.port}` : ''; - let extraInfo = ''; - - // Add extra info for frontend service - if (service === 'frontend' && info.mode) { - extraInfo = ` (${info.mode})`; - } - - const totalLength = serviceName.length + port.length + extraInfo.length; - const line = `${colors.white}║ ${status} ${serviceName}${port}${extraInfo}${' '.repeat(55 - totalLength)}║${colors.reset}`; - console.log(line); - }); - - console.log(`${colors.green}║ ║ -╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); - - if (config.startup.enableFrontend && startupState.services.frontend) { - log('info', `🌐 Game URL: http://${config.frontend.host}:${config.frontend.port}`); - } - - log('info', `📊 API URL: http://${config.backend.host}:${config.backend.port}`); - log('info', `📋 Press Ctrl+C to stop all services`); -} - -/** - * Graceful shutdown handler - */ -function setupGracefulShutdown() { - const shutdown = async (signal) => { - log('warn', `Received ${signal}. Starting graceful shutdown...`); - - try { - // Stop processes - if (processes.frontend) { - log('info', 'Stopping frontend server...'); - processes.frontend.kill('SIGTERM'); - } - - if (processes.backend) { - log('info', 'Stopping backend server...'); - processes.backend.kill('SIGTERM'); - } - - // Wait a moment for graceful shutdown - await new Promise(resolve => setTimeout(resolve, 2000)); - - log('success', 'All services stopped successfully'); - process.exit(0); - } catch (error) { - log('error', 'Error during shutdown:', error); - process.exit(1); - } - }; - - process.on('SIGTERM', () => shutdown('SIGTERM')); - process.on('SIGINT', () => shutdown('SIGINT')); - - process.on('unhandledRejection', (reason, promise) => { - log('error', 'Unhandled Promise Rejection:', { reason, promise: promise.toString() }); - }); - - process.on('uncaughtException', (error) => { - log('error', 'Uncaught Exception:', error); - process.exit(1); - }); -} - -/** - * Main startup function - */ -async function startGame() { - try { - displayBanner(); - setupGracefulShutdown(); - - // Run startup sequence - await runPreflightChecks(); - await validateDatabase(); - await startBackendServer(); - await startFrontendServer(); - await startHealthMonitoring(); - - displayStartupSummary(); - - } catch (error) { - log('error', '💥 Startup failed:', error); - - // Cleanup any started processes - if (processes.backend) processes.backend.kill('SIGTERM'); - if (processes.frontend) processes.frontend.kill('SIGTERM'); - - process.exit(1); - } -} - -// Start the game if this file is run directly -if (require.main === module) { - startGame(); -} - -module.exports = { - startGame, - config, - startupState -}; \ No newline at end of file diff --git a/start.sh b/start.sh deleted file mode 100755 index 98a35da..0000000 --- a/start.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/bin/bash - -# Shattered Void MMO - Shell Startup Wrapper -# -# This script provides a simple shell interface for starting the Shattered Void MMO -# with various options and environment configurations. - -set -e # Exit on any error - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -MAGENTA='\033[0;35m' -CYAN='\033[0;36m' -WHITE='\033[1;37m' -NC='\033[0m' # No Color - -# Configuration -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -NODE_SCRIPT="$SCRIPT_DIR/start-game.js" -LOG_DIR="$SCRIPT_DIR/logs" -PID_FILE="$LOG_DIR/startup.pid" - -# Default environment -DEFAULT_ENV="development" -ENV="${NODE_ENV:-$DEFAULT_ENV}" - -# Function to print colored output -print_color() { - local color=$1 - local message=$2 - echo -e "${color}${message}${NC}" -} - -# Function to print banner -print_banner() { - if [ "${DISABLE_BANNER}" != "true" ]; then - print_color $CYAN "╔═══════════════════════════════════════════════════════════════╗" - print_color $CYAN "║ ║" - print_color $CYAN "║ ${WHITE}SHATTERED VOID MMO LAUNCHER${CYAN} ║" - print_color $CYAN "║ ${WHITE}Post-Collapse Galaxy Strategy Game${CYAN} ║" - print_color $CYAN "║ ║" - print_color $CYAN "╚═══════════════════════════════════════════════════════════════╝" - echo - fi -} - -# Function to show usage -show_usage() { - echo "Usage: $0 [OPTIONS]" - echo - echo "Options:" - echo " -e, --env ENV Set environment (development|production|staging)" - echo " -p, --port PORT Set backend port (default: 3000)" - echo " -f, --frontend-port Set frontend port (default: 5173)" - echo " --no-frontend Disable frontend server" - echo " --no-health Disable health monitoring" - echo " --no-database Disable database checks" - echo " --no-redis Disable Redis" - echo " --skip-preflight Skip pre-flight checks" - echo " --verbose Enable verbose logging" - echo " --debug Enable debug mode" - echo " --no-colors Disable colored output" - echo " --log-file FILE Log output to file" - echo " -h, --help Show this help message" - echo " -v, --version Show version information" - echo - echo "Environment Variables:" - echo " NODE_ENV Environment mode (development|production|staging)" - echo " PORT Backend server port" - echo " FRONTEND_PORT Frontend server port" - echo " DISABLE_FRONTEND Disable frontend (true|false)" - echo " DISABLE_REDIS Disable Redis (true|false)" - echo " DISABLE_DATABASE Disable database (true|false)" - echo " SKIP_PREFLIGHT Skip pre-flight checks (true|false)" - echo " VERBOSE_STARTUP Enable verbose startup (true|false)" - echo - echo "Examples:" - echo " $0 Start in development mode" - echo " $0 --env production Start in production mode" - echo " $0 --no-frontend Start without frontend" - echo " $0 --port 8080 Start backend on port 8080" - echo " $0 --debug --verbose Start with debug and verbose logging" -} - -# Function to show version -show_version() { - if [ -f "$SCRIPT_DIR/package.json" ]; then - local version=$(grep '"version"' "$SCRIPT_DIR/package.json" | cut -d'"' -f4) - print_color $GREEN "Shattered Void MMO v$version" - else - print_color $GREEN "Shattered Void MMO (version unknown)" - fi - - echo "Node.js $(node --version)" - echo "NPM $(npm --version)" - echo "Platform: $(uname -s) $(uname -m)" -} - -# Function to check prerequisites -check_prerequisites() { - print_color $BLUE "🔍 Checking prerequisites..." - - # Check Node.js - if ! command -v node &> /dev/null; then - print_color $RED "❌ Node.js is not installed" - exit 1 - fi - - # Check Node.js version - local node_version=$(node --version | cut -d'v' -f2 | cut -d'.' -f1) - if [ "$node_version" -lt 18 ]; then - print_color $RED "❌ Node.js 18+ required, found version $(node --version)" - exit 1 - fi - - # Check NPM - if ! command -v npm &> /dev/null; then - print_color $RED "❌ NPM is not installed" - exit 1 - fi - - # Check if startup script exists - if [ ! -f "$NODE_SCRIPT" ]; then - print_color $RED "❌ Startup script not found: $NODE_SCRIPT" - exit 1 - fi - - # Check if package.json exists - if [ ! -f "$SCRIPT_DIR/package.json" ]; then - print_color $RED "❌ package.json not found" - exit 1 - fi - - # Check if node_modules exists - if [ ! -d "$SCRIPT_DIR/node_modules" ]; then - print_color $YELLOW "⚠️ node_modules not found, running npm install..." - npm install - fi - - print_color $GREEN "✅ Prerequisites check passed" -} - -# Function to create log directory -setup_logging() { - if [ ! -d "$LOG_DIR" ]; then - mkdir -p "$LOG_DIR" - fi -} - -# Function to check if game is already running -check_running() { - if [ -f "$PID_FILE" ]; then - local pid=$(cat "$PID_FILE") - if kill -0 "$pid" 2>/dev/null; then - print_color $YELLOW "⚠️ Game appears to be already running (PID: $pid)" - print_color $YELLOW " Use 'pkill -f start-game.js' to stop it first" - exit 1 - else - # Remove stale PID file - rm -f "$PID_FILE" - fi - fi -} - -# Function to setup signal handlers -setup_signals() { - trap cleanup SIGINT SIGTERM -} - -# Function to cleanup on exit -cleanup() { - print_color $YELLOW "\n🛑 Received shutdown signal, cleaning up..." - - if [ -f "$PID_FILE" ]; then - local pid=$(cat "$PID_FILE") - if kill -0 "$pid" 2>/dev/null; then - print_color $BLUE " Stopping game process (PID: $pid)..." - kill -TERM "$pid" 2>/dev/null || true - - # Wait for graceful shutdown - local wait_count=0 - while kill -0 "$pid" 2>/dev/null && [ $wait_count -lt 10 ]; do - sleep 1 - wait_count=$((wait_count + 1)) - done - - # Force kill if still running - if kill -0 "$pid" 2>/dev/null; then - print_color $RED " Force stopping game process..." - kill -KILL "$pid" 2>/dev/null || true - fi - fi - rm -f "$PID_FILE" - fi - - print_color $GREEN "✅ Cleanup completed" - exit 0 -} - -# Function to start the game -start_game() { - print_color $GREEN "🚀 Starting Shattered Void MMO..." - print_color $BLUE " Environment: $ENV" - print_color $BLUE " Node.js: $(node --version)" - print_color $BLUE " Working Directory: $SCRIPT_DIR" - echo - - # Export environment variables - export NODE_ENV="$ENV" - - # Change to script directory - cd "$SCRIPT_DIR" - - # Start the game and capture PID - if [ -n "$LOG_FILE" ]; then - print_color $BLUE "📝 Logging to: $LOG_FILE" - node "$NODE_SCRIPT" > "$LOG_FILE" 2>&1 & - else - node "$NODE_SCRIPT" & - fi - - local game_pid=$! - echo "$game_pid" > "$PID_FILE" - - print_color $GREEN "✅ Game started with PID: $game_pid" - - # Wait for the process - wait "$game_pid" - local exit_code=$? - - # Cleanup PID file - rm -f "$PID_FILE" - - if [ $exit_code -eq 0 ]; then - print_color $GREEN "✅ Game exited successfully" - else - print_color $RED "❌ Game exited with error code: $exit_code" - fi - - exit $exit_code -} - -# Function to validate environment -validate_environment() { - case "$ENV" in - development|production|staging|testing) - ;; - *) - print_color $RED "❌ Invalid environment: $ENV" - print_color $YELLOW " Valid environments: development, production, staging, testing" - exit 1 - ;; - esac -} - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - -e|--env) - ENV="$2" - shift 2 - ;; - -p|--port) - export PORT="$2" - shift 2 - ;; - -f|--frontend-port) - export FRONTEND_PORT="$2" - shift 2 - ;; - --no-frontend) - export ENABLE_FRONTEND="false" - shift - ;; - --no-health) - export ENABLE_HEALTH_MONITORING="false" - shift - ;; - --no-database) - export DISABLE_DATABASE="true" - shift - ;; - --no-redis) - export DISABLE_REDIS="true" - shift - ;; - --skip-preflight) - export SKIP_PREFLIGHT="true" - shift - ;; - --verbose) - export VERBOSE_STARTUP="true" - export LOG_LEVEL="debug" - shift - ;; - --debug) - export NODE_ENV="development" - export DEBUG="*" - export VERBOSE_STARTUP="true" - export LOG_LEVEL="debug" - ENV="development" - shift - ;; - --no-colors) - export DISABLE_COLORS="true" - shift - ;; - --log-file) - LOG_FILE="$2" - shift 2 - ;; - -h|--help) - show_usage - exit 0 - ;; - -v|--version) - show_version - exit 0 - ;; - *) - print_color $RED "❌ Unknown option: $1" - echo - show_usage - exit 1 - ;; - esac -done - -# Main execution -main() { - # Show banner - print_banner - - # Validate environment - validate_environment - - # Set up logging - setup_logging - - # Check if already running - check_running - - # Check prerequisites - check_prerequisites - - # Set up signal handlers - setup_signals - - # Start the game - start_game -} - -# Run main function -main "$@" \ No newline at end of file diff --git a/stop-game.js b/stop-game.js deleted file mode 100755 index 5a67653..0000000 --- a/stop-game.js +++ /dev/null @@ -1,377 +0,0 @@ -#!/usr/bin/env node - -/** - * Shattered Void MMO Server Shutdown Script - * Gracefully stops all running game services - */ - -const { spawn, exec } = require('child_process'); -const { promisify } = require('util'); -const execAsync = promisify(exec); - -// Console colors for better output -const colors = { - reset: '\x1b[0m', - bright: '\x1b[1m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m', - white: '\x1b[37m', -}; - -function log(level, message, data = {}) { - const timestamp = new Date().toISOString(); - const logData = Object.keys(data).length > 0 ? ` ${JSON.stringify(data, null, 2)}` : ''; - - let color = colors.white; - let levelStr = level.toUpperCase().padEnd(7); - - switch (level.toLowerCase()) { - case 'info': - color = colors.cyan; - break; - case 'success': - color = colors.green; - break; - case 'warn': - color = colors.yellow; - break; - case 'error': - color = colors.red; - break; - } - - console.log(`${colors.bright}[${timestamp}] [PID:${process.pid}] [${color}${levelStr}${colors.reset}${colors.bright}]${colors.reset} ${color}${message}${colors.reset}${logData}`); -} - -function displayHeader() { - console.log(`${colors.cyan}╔═══════════════════════════════════════════════════════════════╗ -║ ║ -║ ${colors.bright}SHATTERED VOID MMO SHUTDOWN${colors.reset}${colors.cyan} ║ -║ ${colors.white}Post-Collapse Galaxy Strategy Game${colors.reset}${colors.cyan} ║ -║ ║ -║ ${colors.yellow}Gracefully stopping all running services...${colors.reset}${colors.cyan} ║ -║ ║ -╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); - console.log(); -} - -async function findProcesses() { - log('info', 'Scanning for running game processes...'); - - const processes = []; - - try { - // Look for the main startup script - const { stdout: startupProcs } = await execAsync('ps aux | grep "node.*start-game.js" | grep -v grep || true'); - if (startupProcs.trim()) { - const lines = startupProcs.trim().split('\n'); - for (const line of lines) { - const parts = line.trim().split(/\s+/); - const pid = parts[1]; - processes.push({ - pid, - command: 'start-game.js', - type: 'main', - description: 'Main startup orchestrator' - }); - } - } - - // Look for Node.js processes on our ports - const { stdout: nodeProcs } = await execAsync('ps aux | grep "node" | grep -E "(3000|5173)" | grep -v grep || true'); - if (nodeProcs.trim()) { - const lines = nodeProcs.trim().split('\n'); - for (const line of lines) { - const parts = line.trim().split(/\s+/); - const pid = parts[1]; - if (!processes.find(p => p.pid === pid)) { - processes.push({ - pid, - command: 'node (port 3000/5173)', - type: 'server', - description: 'Backend/Frontend server' - }); - } - } - } - - // Look for npm processes - const { stdout: npmProcs } = await execAsync('ps aux | grep "npm.*dev" | grep -v grep || true'); - if (npmProcs.trim()) { - const lines = npmProcs.trim().split('\n'); - for (const line of lines) { - const parts = line.trim().split(/\s+/); - const pid = parts[1]; - if (!processes.find(p => p.pid === pid)) { - processes.push({ - pid, - command: 'npm dev', - type: 'dev', - description: 'NPM development server' - }); - } - } - } - - // Look for Vite processes - const { stdout: viteProcs } = await execAsync('ps aux | grep "vite" | grep -v grep || true'); - if (viteProcs.trim()) { - const lines = viteProcs.trim().split('\n'); - for (const line of lines) { - const parts = line.trim().split(/\s+/); - const pid = parts[1]; - if (!processes.find(p => p.pid === pid)) { - processes.push({ - pid, - command: 'vite', - type: 'frontend', - description: 'Vite development server' - }); - } - } - } - - // Look for Python servers (static file serving) - const { stdout: pythonProcs } = await execAsync('ps aux | grep "python.*http.server" | grep -v grep || true'); - if (pythonProcs.trim()) { - const lines = pythonProcs.trim().split('\n'); - for (const line of lines) { - const parts = line.trim().split(/\s+/); - const pid = parts[1]; - if (!processes.find(p => p.pid === pid)) { - processes.push({ - pid, - command: 'python http.server', - type: 'static', - description: 'Static file server' - }); - } - } - } - - } catch (error) { - log('warn', 'Error scanning for processes:', { error: error.message }); - } - - return processes; -} - -async function checkPorts() { - log('info', 'Checking port usage...'); - - const ports = []; - - try { - const { stdout } = await execAsync('ss -tlnp | grep ":3000\\|:5173" || true'); - if (stdout.trim()) { - const lines = stdout.trim().split('\n'); - for (const line of lines) { - const match = line.match(/:(\d+)\s.*users:\(\("([^"]+)",pid=(\d+)/); - if (match) { - const [, port, process, pid] = match; - ports.push({ port, process, pid }); - } - } - } - } catch (error) { - log('warn', 'Error checking ports:', { error: error.message }); - } - - return ports; -} - -async function stopProcess(process) { - log('info', `Stopping ${process.description}`, { pid: process.pid, command: process.command }); - - try { - // Try graceful shutdown first (SIGTERM) - await execAsync(`kill -TERM ${process.pid}`); - - // Wait a moment for graceful shutdown - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Check if process is still running - try { - await execAsync(`kill -0 ${process.pid}`); - log('warn', `Process ${process.pid} still running, forcing shutdown...`); - await execAsync(`kill -KILL ${process.pid}`); - } catch (error) { - // Process already stopped - } - - log('success', `Stopped ${process.description}`, { pid: process.pid }); - return true; - } catch (error) { - if (error.message.includes('No such process')) { - log('info', `Process ${process.pid} already stopped`); - return true; - } - log('error', `Failed to stop process ${process.pid}:`, { error: error.message }); - return false; - } -} - -async function verifyShutdown() { - log('info', 'Verifying complete shutdown...'); - - const remainingProcesses = await findProcesses(); - const remainingPorts = await checkPorts(); - - if (remainingProcesses.length === 0 && remainingPorts.length === 0) { - log('success', '✅ All game services successfully stopped'); - return true; - } else { - if (remainingProcesses.length > 0) { - log('warn', `${remainingProcesses.length} processes still running:`, { - processes: remainingProcesses.map(p => ({ pid: p.pid, command: p.command })) - }); - } - if (remainingPorts.length > 0) { - log('warn', `${remainingPorts.length} ports still in use:`, { - ports: remainingPorts.map(p => ({ port: p.port, process: p.process, pid: p.pid })) - }); - } - return false; - } -} - -async function main() { - const startTime = Date.now(); - - try { - displayHeader(); - - // Phase 1: Discovery - log('info', 'Phase 1: Discovering running services'); - const processes = await findProcesses(); - const ports = await checkPorts(); - - if (processes.length === 0 && ports.length === 0) { - log('info', '🎯 No running game services found'); - log('success', '✅ System is already clean'); - return; - } - - log('info', `Found ${processes.length} processes and ${ports.length} active ports`); - - if (processes.length > 0) { - console.log('\n📋 Processes to stop:'); - processes.forEach(proc => { - console.log(` • ${colors.yellow}${proc.description}${colors.reset} (PID: ${proc.pid}) - ${proc.command}`); - }); - } - - if (ports.length > 0) { - console.log('\n🔌 Ports to free:'); - ports.forEach(port => { - console.log(` • Port ${colors.cyan}${port.port}${colors.reset} used by ${port.process} (PID: ${port.pid})`); - }); - } - - console.log(); - - // Phase 2: Graceful shutdown - log('info', 'Phase 2: Graceful service shutdown'); - - let stopCount = 0; - let failCount = 0; - - // Stop processes in order of importance (main process first) - const processOrder = ['main', 'server', 'dev', 'frontend', 'static']; - for (const type of processOrder) { - const processesOfType = processes.filter(p => p.type === type); - for (const process of processesOfType) { - const success = await stopProcess(process); - if (success) { - stopCount++; - } else { - failCount++; - } - } - } - - // Phase 3: Verification - log('info', 'Phase 3: Verification and cleanup'); - const cleanShutdown = await verifyShutdown(); - - // Final summary - const duration = Date.now() - startTime; - console.log(); - - if (cleanShutdown) { - console.log(`${colors.green}╔═══════════════════════════════════════════════════════════════╗ -║ SHUTDOWN COMPLETE ║ -╠═══════════════════════════════════════════════════════════════╣${colors.reset} -${colors.white}║ Duration: ${duration}ms${' '.repeat(50 - duration.toString().length)}║ -║ ║${colors.reset} -${colors.cyan}║ Services Stopped: ║${colors.reset} -${colors.white}║ ✅ All processes terminated ║ -║ ✅ All ports freed ║ -║ ✅ System clean ║${colors.reset} -${colors.green}║ ║ -╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); - - log('info', '🎮 Game services stopped successfully'); - log('info', '💡 Run "node start-game.js" to restart the game'); - } else { - console.log(`${colors.yellow}╔═══════════════════════════════════════════════════════════════╗ -║ SHUTDOWN INCOMPLETE ║ -╠═══════════════════════════════════════════════════════════════╣${colors.reset} -${colors.white}║ Duration: ${duration}ms${' '.repeat(50 - duration.toString().length)}║ -║ ║${colors.reset} -${colors.white}║ Stopped: ${stopCount} processes${' '.repeat(42 - stopCount.toString().length)}║ -║ Failed: ${failCount} processes${' '.repeat(43 - failCount.toString().length)}║${colors.reset} -${colors.yellow}║ ║ -║ Some services may still be running. ║ -║ Check the warnings above for details. ║ -║ ║ -╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); - - log('warn', '⚠️ Some services may still be running'); - log('info', '💡 You may need to manually stop remaining processes'); - process.exit(1); - } - - } catch (error) { - const duration = Date.now() - startTime; - log('error', 'Shutdown script failed:', { - error: error.message, - stack: error.stack, - duration: `${duration}ms` - }); - - console.log(`${colors.red}╔═══════════════════════════════════════════════════════════════╗ -║ SHUTDOWN FAILED ║ -╠═══════════════════════════════════════════════════════════════╣${colors.reset} -${colors.white}║ Duration: ${duration}ms${' '.repeat(50 - duration.toString().length)}║ -║ ║${colors.reset} -${colors.red}║ An error occurred during shutdown. ║ -║ Some services may still be running. ║ -║ ║ -╚═══════════════════════════════════════════════════════════════╝${colors.reset}`); - - process.exit(1); - } -} - -// Handle script interruption -process.on('SIGINT', () => { - log('warn', 'Shutdown script interrupted'); - process.exit(1); -}); - -process.on('SIGTERM', () => { - log('warn', 'Shutdown script terminated'); - process.exit(1); -}); - -// Run the shutdown script -if (require.main === module) { - main(); -} - -module.exports = { main }; \ No newline at end of file diff --git a/test_auth.html b/test_auth.html deleted file mode 100644 index accdcbb..0000000 --- a/test_auth.html +++ /dev/null @@ -1,224 +0,0 @@ -<\!DOCTYPE html> - - - - - Shattered Void - Auth Test - - - -

🌌 Shattered Void - Authentication Test

- -
-

Registration

-
- - -
-
- - -
-
- - -
- -
- -
-

Login

-
- - -
-
- - -
- -
- -
-

User Profile (requires login)

- - -
- -
- - - - \ No newline at end of file diff --git a/test_frontend_api.html b/test_frontend_api.html deleted file mode 100644 index d7a874b..0000000 --- a/test_frontend_api.html +++ /dev/null @@ -1,165 +0,0 @@ -<\!DOCTYPE html> - - - Frontend-Backend API Test - - - -
-

Shattered Void Frontend-Backend Test

- -
-

1. Backend Health Check

- -
-
- -
-

2. Registration Test

-
- - - -
- -
-
- -
-

3. Login Test

-
- - -
- -
-
- -
-

4. CORS Test

- -
-
-
- - - - -EOF < /dev/null diff --git a/test_registration.html b/test_registration.html deleted file mode 100644 index 3e5c17c..0000000 --- a/test_registration.html +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - Test Registration - Shattered Void MMO - - - -

🎮 Shattered Void MMO - Registration Test

- -
-
- - -
- -
- - -
- -
- - - Must contain uppercase, lowercase, number, and special character -
- -
- - -
- - -
- -
- -
- -

Login Test

-
-
- - -
- -
- - -
- - -
- -
- - - - \ No newline at end of file diff --git a/verify-db-connection.js b/verify-db-connection.js deleted file mode 100644 index 9cffaf6..0000000 --- a/verify-db-connection.js +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env node - -/** - * PostgreSQL Connection Verification Script - * This script verifies the database connection using the current environment configuration - */ - -require('dotenv').config({ path: `.env.${process.env.NODE_ENV || 'development'}` }); -const { Client } = require('pg'); - -async function verifyConnection() { - console.log('🔍 PostgreSQL Connection Verification'); - console.log('====================================='); - console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); - console.log(`Host: ${process.env.DB_HOST || 'localhost'}`); - console.log(`Port: ${process.env.DB_PORT || 5432}`); - console.log(`Database: ${process.env.DB_NAME || 'shattered_void_dev'}`); - console.log(`User: ${process.env.DB_USER || 'postgres'}`); - console.log(`Password: ${'*'.repeat((process.env.DB_PASSWORD || 'password').length)}`); - console.log('=====================================\n'); - - const client = new Client({ - host: process.env.DB_HOST || 'localhost', - port: process.env.DB_PORT || 5432, - database: process.env.DB_NAME || 'shattered_void_dev', - user: process.env.DB_USER || 'postgres', - password: process.env.DB_PASSWORD || 'password', - }); - - try { - console.log('🔌 Attempting to connect...'); - await client.connect(); - console.log('✅ Connected successfully!'); - - console.log('\n🔍 Testing basic queries...'); - - // Test 1: Get PostgreSQL version - const versionResult = await client.query('SELECT version()'); - console.log('✅ Version query successful'); - console.log(` PostgreSQL Version: ${versionResult.rows[0].version.split(' ')[0]} ${versionResult.rows[0].version.split(' ')[1]}`); - - // Test 2: Check if we can create a test table (and clean it up) - console.log('\n🧪 Testing table creation permissions...'); - await client.query(` - CREATE TABLE IF NOT EXISTS connection_test_${Date.now()} ( - id SERIAL PRIMARY KEY, - test_data VARCHAR(50) - ) - `); - console.log('✅ Table creation successful'); - - // Test 3: Check database existence - const dbCheckResult = await client.query(` - SELECT datname FROM pg_database WHERE datname = $1 - `, [process.env.DB_NAME || 'shattered_void_dev']); - - if (dbCheckResult.rows.length > 0) { - console.log('✅ Target database exists'); - } else { - console.log('⚠️ Target database does not exist - you may need to create it'); - } - - // Test 4: Check for existing game tables - console.log('\n🎮 Checking for existing game tables...'); - const tablesResult = await client.query(` - SELECT table_name - FROM information_schema.tables - WHERE table_schema = 'public' - AND table_type = 'BASE TABLE' - ORDER BY table_name - `); - - if (tablesResult.rows.length > 0) { - console.log('✅ Found existing tables:'); - tablesResult.rows.forEach(row => { - console.log(` - ${row.table_name}`); - }); - } else { - console.log('ℹ️ No game tables found - database may need to be migrated'); - } - - console.log('\n🎉 All connection tests passed!'); - console.log('🚀 Your PostgreSQL configuration is working correctly.'); - - } catch (error) { - console.error('\n❌ Connection failed!'); - console.error('Error details:'); - console.error(` Type: ${error.code || 'Unknown'}`); - console.error(` Message: ${error.message}`); - - if (error.code === '28P01') { - console.error('\n🔧 SOLUTION: Password authentication failed'); - console.error(' This means the password in your .env file doesn\'t match the PostgreSQL user password.'); - console.error(' Please run the sudo commands provided earlier to set the correct password.'); - } else if (error.code === 'ECONNREFUSED') { - console.error('\n🔧 SOLUTION: Connection refused'); - console.error(' PostgreSQL service is not running. Try:'); - console.error(' sudo systemctl start postgresql'); - } else if (error.code === '3D000') { - console.error('\n🔧 SOLUTION: Database does not exist'); - console.error(' Create the database with:'); - console.error(` sudo -u postgres createdb ${process.env.DB_NAME || 'shattered_void_dev'}`); - } - - process.exit(1); - } finally { - await client.end(); - console.log('\n🔌 Connection closed.'); - } -} - -// Run the verification -if (require.main === module) { - verifyConnection().catch(console.error); -} - -module.exports = { verifyConnection }; \ No newline at end of file