diff --git a/.gitignore b/.gitignore deleted file mode 100644 index f557b1f8..00000000 --- a/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -#ignore each file in folder that starts with Archive_ -Archive_* -#ignore each folder that starts with Archive_ -Archive_*/** -#ignore node_modules folder -**/node_modules/** - -#ignore dist folder -**/dist/** - -#ignore log files -**/*.log diff --git a/Documentations/BUILD.md b/Documentations/BUILD.md deleted file mode 100644 index 4abf3b59..00000000 --- a/Documentations/BUILD.md +++ /dev/null @@ -1,297 +0,0 @@ -# SerpentRace Backend Build System - -## Overview - -This document describes the comprehensive build system for the SerpentRace backend application. The build system handles TypeScript compilation, database migrations, asset management, testing, and deployment. - -## Quick Start - -```bash -# Development build -npm run build - -# Production build with full validation -npm run build:production - -# Advanced build with migrations and tests -npm run build:advanced:prod - -# Development server with hot reload -npm run dev -``` - -## Build Scripts - -### Basic Build Commands - -| Command | Description | -|---------|-------------| -| `npm run build` | Standard build: clean → compile → copy assets | -| `npm run build:clean` | Clean the dist directory | -| `npm run build:compile` | Compile TypeScript to JavaScript | -| `npm run build:copy-assets` | Copy non-TS files to dist directory | -| `npm run build:docker` | Build for Docker (no tests/migrations) | - -### Production Build Commands - -| Command | Description | -|---------|-------------| -| `npm run build:production` | Full production build with linting, tests, and migrations | -| `npm run build:advanced` | Advanced build script with custom options | -| `npm run build:advanced:prod` | Advanced production build with all validations | -| `npm run build:advanced:ci` | CI/CD friendly build (skips linting) | - -### Development Commands - -| Command | Description | -|---------|-------------| -| `npm run dev` | Start development server with hot reload | -| `npm run watch` | Watch mode TypeScript compilation | -| `npm run typecheck` | Type checking without code generation | - -### Database Commands - -| Command | Description | -|---------|-------------| -| `npm run migration:run` | Run pending database migrations | -| `npm run migration:show` | Show migration status | -| `npm run migration:generate ` | Generate new migration | -| `npm run migration:create ` | Create empty migration | -| `npm run migration:revert` | Revert last migration | -| `npm run migration:full ` | Create, generate, and run migration | - -### Testing Commands - -| Command | Description | -|---------|-------------| -| `npm test` | Run all tests | -| `npm run test:watch` | Run tests in watch mode | -| `npm run test:coverage` | Run tests with coverage report | -| `npm run test:redis` | Run Redis-specific tests | - -### Deployment Commands - -| Command | Description | -|---------|-------------| -| `npm run deploy:prod` | Build for production deployment | -| `scripts/deploy.sh` | Full Linux/Mac deployment script | -| `scripts/deploy.bat` | Full Windows deployment script | - -## Advanced Build Script - -The advanced build script (`scripts/build.ts`) supports various options: - -```bash -# Basic advanced build -npm run build:advanced - -# Production build with migrations and tests -npm run build:advanced:prod - -# CI/CD build (skips linting, includes tests and migrations) -npm run build:advanced:ci -``` - -### Build Options - -- `--migrations`: Run database migrations during build -- `--test`: Run tests during build -- `--skip-lint`: Skip linting step -- `--production`: Enable production mode (strict validation) - -## Deployment Scripts - -### Linux/Mac Deployment - -```bash -./scripts/deploy.sh [deploy|build-only|test-connections] -``` - -Options: -- `deploy` (default): Full deployment with validation -- `build-only`: Build without connection testing -- `test-connections`: Test database and Redis connections only - -### Windows Deployment - -```cmd -scripts\deploy.bat [deploy|build-only|test-connections] -``` - -Same options as Linux/Mac version. - -### Required Environment Variables - -The deployment scripts require these environment variables: - -```bash -DB_HOST=localhost -DB_PORT=5432 -DB_USERNAME=postgres -DB_PASSWORD=your_password -DB_NAME=serpentrace -JWT_SECRET=your_jwt_secret -REDIS_HOST=localhost -REDIS_PORT=6379 -``` - -## Build Process Flow - -### Standard Build (`npm run build`) - -1. **Clean** - Remove previous build artifacts -2. **Lint** - Code quality checks (if configured) -3. **Compile** - TypeScript compilation -4. **Copy Assets** - Copy non-TS files to dist -5. **Post-build** - Validation and cleanup - -### Production Build (`npm run build:production`) - -1. **Clean** - Remove previous build artifacts -2. **Lint** - Code quality checks -3. **Test** - Run test suite -4. **Migrations** - Apply database migrations -5. **Compile** - TypeScript compilation -6. **Copy Assets** - Copy non-TS files to dist -7. **Validate** - Ensure build integrity - -### Advanced Build (`npm run build:advanced`) - -Provides fine-grained control over the build process with comprehensive logging and error handling. - -## Asset Management - -The build system automatically copies these file types to the dist directory: - -- `.json` files (configuration, data) -- `.html` files (templates) -- `.css` files (stylesheets) -- Image files (`.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`) -- Font files (`.woff`, `.woff2`, `.ttf`, `.eot`) - -Excluded directories: -- `node_modules` -- `.git` -- `tests` -- `__tests__` - -## TypeScript Configuration - -The build system uses the following TypeScript settings: - -- **Target**: ES2020 -- **Module**: CommonJS -- **Output Directory**: `./dist` -- **Source Maps**: Enabled -- **Declarations**: Enabled for type definitions -- **Strict Mode**: Enabled for type safety - -## Migration Management - -### Creating Migrations - -```bash -# Create empty migration -npm run migration:create AddNewTable - -# Generate migration from entity changes -npm run migration:generate AddNewTable - -# Full migration workflow (create + generate + run) -npm run migration:full AddNewTable -``` - -### Migration Best Practices - -1. Always backup database before running migrations in production -2. Test migrations in development environment first -3. Use descriptive migration names -4. Review generated migrations before running them - -## Docker Integration - -The build system is optimized for Docker deployments: - -```dockerfile -# Use build:docker for container builds -RUN npm run build:docker - -# Or use production build for full validation -RUN npm run build:production -``` - -## Troubleshooting - -### Common Issues - -1. **Build fails with "Cannot find module"** - - Run `npm ci` to ensure all dependencies are installed - - Check TypeScript paths configuration - -2. **Migration errors during build** - - Verify database connection parameters - - Ensure database exists and is accessible - - Check migration files for syntax errors - -3. **Asset copying fails** - - Verify file permissions - - Check disk space availability - - Ensure source files exist - -4. **TypeScript compilation errors** - - Run `npm run typecheck` for detailed error messages - - Check tsconfig.json configuration - - Verify all type definitions are installed - -### Debug Mode - -Enable verbose logging by setting the environment variable: - -```bash -export DEBUG=serpentrace:* -npm run build:advanced -``` - -## Performance Optimization - -### Build Performance Tips - -1. Use `npm ci` instead of `npm install` in CI/CD -2. Enable TypeScript incremental compilation for development -3. Use `--skip-lint` in CI if linting is handled separately -4. Cache node_modules in CI/CD pipelines - -### Runtime Performance - -The build system optimizes the output for production: - -- Source maps for debugging (can be disabled in production) -- Type declarations for library usage -- Compressed and optimized JavaScript output - -## Monitoring and Logging - -Build logs include: -- Timestamps for each build step -- Error details with stack traces -- Performance metrics (build duration) -- Validation results - -Production builds create detailed logs in the `logs/` directory. - -## Contributing - -When modifying the build system: - -1. Test changes with both development and production builds -2. Update this documentation for any new scripts or options -3. Ensure backward compatibility -4. Add appropriate error handling and logging - -## Support - -For build system issues: -1. Check this documentation -2. Review error logs in the console -3. Verify environment variables are set correctly -4. Test with a clean `node_modules` installation diff --git a/Documentations/COMPLETE_GAME_WORKFLOW.md b/Documentations/COMPLETE_GAME_WORKFLOW.md deleted file mode 100644 index eac61973..00000000 --- a/Documentations/COMPLETE_GAME_WORKFLOW.md +++ /dev/null @@ -1,2228 +0,0 @@ -# Complete Game Workflow Documentation - -**Version**: 2.0 -**Last Updated**: October 30, 2025 -**Document Type**: Complete System Reference - ---- - -## Table of Contents - -1. [System Overview](#system-overview) -2. [Game Flow Diagram](#game-flow-diagram) -3. [REST API Endpoints](#rest-api-endpoints) -4. [WebSocket Events](#websocket-events) -5. [Interfaces & Data Structures](#interfaces--data-structures) -6. [Domain Aggregates](#domain-aggregates) -7. [Complete Game Scenarios](#complete-game-scenarios) -8. [Turn Tracking System](#turn-tracking-system) -9. [Position Guessing Mechanic](#position-guessing-mechanic) -10. [Error Handling](#error-handling) - ---- - -## System Overview - -The SerpentRace game system uses a **hybrid architecture**: -- **REST API**: Game creation, joining, and initial setup -- **WebSocket (Socket.IO)**: Real-time gameplay, card interactions, and game state updates -- **Redis**: Session management, pending states, turn tracking -- **PostgreSQL**: Persistent game data, user profiles, decks - -### Technology Stack -- **Backend**: Node.js + TypeScript + Express -- **Real-time**: Socket.IO (WebSocket library) -- **Database**: TypeORM + PostgreSQL -- **Cache/Session**: Redis -- **Authentication**: JWT tokens (Access + Refresh) - ---- - -## Game Flow Diagram - -``` -┌──────────────────────────────────────────────────────────────────────────┐ -│ GAME LIFECYCLE │ -└──────────────────────────────────────────────────────────────────────────┘ - -1. GAME CREATION (REST) - │ - ├─ POST /api/games/start - │ ├─ Gamemaster creates game with deck selection - │ ├─ Game Code generated (6 characters) - │ └─ Game state: "waiting" - │ - ▼ - -2. PLAYER JOINING (REST + WebSocket) - │ - ├─ POST /api/games/join - │ ├─ Validate game code - │ ├─ Check game type (PUBLIC/PRIVATE/ORGANIZATION) - │ ├─ Add player to game.players[] - │ └─ Return gameToken for WebSocket auth - │ - ├─ WebSocket Connection - │ ├─ Connect to /game namespace - │ ├─ Emit: game:join with gameToken - │ ├─ Server validates & joins rooms - │ └─ Broadcast: game:player-joined to all - │ - ├─ Emit: game:ready (player marks ready) - └─ Broadcast: game:player-ready to all - │ - ▼ - -3. GAME START (REST) - │ - ├─ POST /api/games/:gameId/start - │ ├─ Only gamemaster can start - │ ├─ Check minimum players (2+) - │ ├─ Generate board (100 fields with pattern) - │ ├─ Shuffle and assign turn sequence - │ ├─ Initialize player positions (all at 0) - │ └─ Game state: "active" - │ - ├─ Broadcast: game:started - │ ├─ Board data sent to all players - │ ├─ Turn sequence revealed - │ └─ First player notified - │ - ▼ - -4. GAMEPLAY LOOP (WebSocket) - │ - ├─ DICE ROLL - │ ├─ Emit: game:dice-roll { diceValue: 1-6 } - │ ├─ Broadcast: game:dice-rolled - │ ├─ Calculate new position - │ ├─ Update player position - │ ├─ Broadcast: game:player-moved - │ └─ Check field type at new position - │ - ├─ SPECIAL FIELD LANDING (positive/negative/luck) - │ ├─ Wait 2 seconds (frontend animation) - │ ├─ Draw card from appropriate deck - │ ├─ Broadcast: game:card-drawn - │ └─ Branch based on card type: - │ - │ ┌─ QUESTION CARD (types 0-4) - │ │ ├─ Emit to player: game:card-drawn-self (60s) - │ │ ├─ Player answers - │ │ ├─ Emit: game:card-answer - │ │ ├─ Broadcast: game:answer-submitted - │ │ ├─ Validate answer - │ │ ├─ Broadcast: game:answer-validated - │ │ ├─ Determine if guess required: - │ │ │ ├─ Positive field + correct = GUESS - │ │ │ ├─ Negative field + wrong = GUESS - │ │ │ ├─ Positive field + wrong = NO MOVEMENT - │ │ │ └─ Negative field + correct = NO MOVEMENT - │ │ │ - │ │ └─ IF GUESS REQUIRED: - │ │ ├─ Emit: game:position-guess-request (30s) - │ │ ├─ Show pattern modifier, dice, stepValue - │ │ ├─ Player guesses position - │ │ ├─ Emit: game:position-guess - │ │ ├─ Broadcast: game:position-guess-broadcast - │ │ ├─ Calculate with BoardGenerationService - │ │ ├─ Apply -2 penalty if wrong - │ │ ├─ Broadcast: game:guess-result - │ │ ├─ Check win condition (position >= 100) - │ │ └─ Check secondary landing - │ - │ ┌─ LUCK CARD (type 6) - │ │ ├─ Process immediately (no answer) - │ │ ├─ Broadcast: game:card-result - │ │ ├─ Apply consequence: - │ │ │ ├─ MOVE_FORWARD (0): Move X steps - │ │ │ ├─ MOVE_BACKWARD (1): Move X steps back - │ │ │ ├─ LOSE_TURN (2): Store X turns to lose - │ │ │ ├─ EXTRA_TURN (3): Store X extra turns - │ │ │ └─ GO_TO_START (5): Return to position 1 - │ │ ├─ Broadcast: game:luck-consequence - │ │ └─ Advance turn (with turn tracking) - │ - │ └─ JOKER CARD (type 5) - Secondary Landing Only - │ ├─ Broadcast: game:joker-drawn - │ ├─ Request gamemaster decision - │ ├─ Emit to gamemaster: game:gamemaster-decision-request (120s) - │ ├─ Gamemaster decides (approve/reject) - │ ├─ Emit: game:gamemaster-decision - │ ├─ Broadcast: game:gamemaster-decision-result - │ ├─ Determine if guess required: - │ │ ├─ Positive field + approved = GUESS - │ │ ├─ Negative field + rejected = GUESS - │ │ ├─ Positive field + rejected = NO MOVEMENT - │ │ └─ Negative field + approved = NO MOVEMENT - │ │ - │ └─ IF GUESS REQUIRED: - │ ├─ Emit: game:joker-position-guess-request (30s) - │ ├─ Player guesses with dice=6 - │ ├─ Emit: game:joker-position-guess - │ ├─ Calculate with BoardGenerationService - │ ├─ Apply -2 penalty if wrong - │ ├─ Broadcast: game:joker-complete - │ └─ Check secondary landing - │ - ├─ TURN ADVANCEMENT - │ ├─ Check if current player has extra turns - │ │ └─ IF YES: Same player continues, decrement counter - │ ├─ Find next player in sequence - │ ├─ Skip players with turns to lose - │ │ └─ Broadcast: game:players-skipped - │ ├─ Broadcast: game:turn-changed - │ └─ Emit to next player: game:your-turn - │ - └─ WIN CONDITION - ├─ Player reaches position >= 100 - ├─ Broadcast: game:ended - ├─ Update database (finished=true, winner) - └─ Cleanup all Redis data and disconnect - │ - ▼ - -5. GAME END & CLEANUP - │ - ├─ Clear all Redis keys: - │ ├─ gameplay:{gameCode} - │ ├─ game_positions:{gameCode} - │ ├─ pending_card:{gameCode}:{playerId} - │ ├─ pending_decision:{gameCode}:{requestId} - │ ├─ player_extra_turns:{gameCode}:{playerId} - │ └─ player_turns_to_lose:{gameCode}:{playerId} - │ - ├─ Force disconnect all players from rooms - ├─ Emit: game:cleanup-complete - └─ Database updated with final state -``` - ---- - -## REST API Endpoints - -### Authentication Headers -All authenticated endpoints require: -``` -Authorization: Bearer -``` - -### 1. Create Game - -**Endpoint**: `POST /api/games/start` -**Auth**: Required -**Description**: Create a new game session with selected decks - -**Request Body**: -```typescript -{ - deckids: string[]; // Array of deck UUIDs (at least 1) - maxplayers: number; // Maximum players (2-10) - logintype: number; // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION -} -``` - -**Response** (200 OK): -```typescript -{ - id: string; // Game UUID - gamecode: string; // 6-character game code - maxplayers: number; - logintype: number; - boardsize: number; // Default 100 - createdby: string; // Gamemaster user ID - orgid: string | null; // Organization ID (for ORG games) - gamedecks: GameDeck[]; // Array of decks with cards - players: string[]; // Empty array initially - startdate: Date | null; - enddate: Date | null; - finished: boolean; - winner: string | null; - totaltiles: number; -} -``` - -**Errors**: -- `400`: Invalid input (missing deckids, invalid maxplayers, etc.) -- `401`: Not authenticated -- `404`: Deck not found -- `500`: Internal server error - ---- - -### 2. Join Game - -**Endpoint**: `POST /api/games/join` -**Auth**: Optional (depends on game type) -**Description**: Join an existing game using game code - -**Request Body**: -```typescript -{ - gameCode: string; // 6-character game code - playerName?: string; // Required for PUBLIC, optional for authenticated -} -``` - -**Authentication Requirements by Game Type**: -- **PUBLIC (0)**: No auth required, playerName mandatory -- **PRIVATE (1)**: Auth required -- **ORGANIZATION (2)**: Auth + organization membership required - -**Response** (200 OK): -```typescript -{ - id: string; // Game UUID - gamecode: string; // Game code - playerName: string; // Player's display name - playerCount: number; // Current number of players - maxPlayers: number; - gameType: string; // "PUBLIC" | "PRIVATE" | "ORGANIZATION" - isAuthenticated: boolean; - gameToken: string; // JWT token for WebSocket authentication -} -``` - -**Game Token Payload**: -```typescript -{ - gameId: string; - gameCode: string; - playerName: string; - playerId?: string; // Only if authenticated - iat: number; // Issued at - exp: number; // Expires in 24 hours -} -``` - -**Errors**: -- `400`: Invalid gameCode format or missing playerName -- `401`: Authentication required (PRIVATE/ORGANIZATION games) -- `403`: Organization membership required or mismatch -- `404`: Game not found -- `409`: Game full or already started -- `500`: Internal server error - ---- - -### 3. Start Game Play - -**Endpoint**: `POST /api/games/:gameId/start` -**Auth**: Required (only gamemaster) -**Description**: Start the actual gameplay after all players are ready - -**Path Parameters**: -- `gameId`: UUID of the game - -**Request Body**: Empty `{}` - -**Response** (200 OK): -```typescript -{ - message: string; // "Game started successfully" - gameId: string; - playerCount: number; - game: GameAggregate; // Full game object - boardData: { - gameId: string; - fields: GameField[]; // 100 fields with positions, types, stepValues - generationComplete: boolean; - generatedAt: Date; - } -} -``` - -**Board Generation**: -- Generates 100 fields with pattern-based distribution -- Field types: `regular`, `positive`, `negative`, `luck` -- Each field has `stepValue` (0-3) for movement calculations -- Pattern modifiers by zones: - - Positions 1-20: +2 (easier start) - - Positions 21-40: -1 (early challenge) - - Positions 41-60: +1 (mid-game boost) - - Positions 61-80: -2 (late challenge) - - Positions 81-100: +3 (final stretch) - -**Game State Changes**: -- `game.startdate` set to current date -- `game.gamePhase` set to "active" -- Turn sequence randomized -- Player positions initialized to 0 - -**Errors**: -- `400`: Invalid gameId -- `403`: Only gamemaster can start game -- `404`: Game not found -- `409`: Game already started, not enough players, or players not ready -- `500`: Board generation failed or internal error - ---- - -## WebSocket Events - -### Connection & Authentication - -**Namespace**: `/game` - -**Connection**: -```typescript -const socket = io('http://localhost:3000/game', { - auth: { - token: gameToken // JWT token from /game/join endpoint - } -}); -``` - -**Authentication Flow**: -1. Client connects with `gameToken` in auth -2. Server validates token using `GameTokenService` -3. Server extracts: `gameId`, `gameCode`, `playerName`, `playerId` -4. Server joins socket to rooms: - - `game_{gameCode}` (all players) - - `game_{gameCode}:{playerName}` (individual player) -5. Server emits `authenticated` event - -**Events**: - -```typescript -// Client → Server: Initial join -socket.emit('game:join', { gameToken: string }); - -// Server → Client: Authentication success (renamed from 'authenticated') -socket.on('game:joined', { - gameCode: string; - playerName: string; - message: string; - gameId: string; - playerId?: string; - timestamp: string; -}); - -// Server → All Players: Player joined -socket.on('game:player-joined', { - playerId: string; - playerName: string; - playerCount: number; - timestamp: string; -}); -``` - ---- - -### Pre-Game Events - -#### Ready System - -```typescript -// Client → Server: Mark player as ready -socket.emit('game:ready', { - gameCode: string; - ready: boolean; -}); - -// Server → All Players: Player ready status changed -socket.on('game:player-ready', { - playerId: string; - playerName: string; - ready: boolean; - readyCount: number; - totalPlayers: number; - allReady: boolean; - timestamp: string; -}); - -// Server → All Players: All players ready (can start game) -socket.on('game:all-ready', { - message: string; - readyCount: number; - totalPlayers: number; - timestamp: string; -}); -``` - -#### Player Approval System (Private Games Only) - -```typescript -// Server → Pending Player: Waiting for gamemaster approval -socket.on('game:pending-approval', { - message: string; - gameCode: string; - timestamp: string; -}); - -// Server → Gamemaster: Player requesting to join -socket.on('game:player-requesting-join', { - playerName: string; - playerId?: string; - message: string; - timestamp: string; -}); - -// Client → Server: Gamemaster approves player -socket.emit('game:approve-player', { - gameCode: string; - playerName: string; -}); - -// Client → Server: Gamemaster rejects player -socket.emit('game:reject-player', { - gameCode: string; - playerName: string; - reason?: string; -}); - -// Server → Approved Player: Join approved, can reconnect -socket.on('game:approval-granted', { - gameCode: string; - playerName: string; - message: string; - timestamp: string; -}); - -// Server → Rejected Player: Join denied -socket.on('game:approval-denied', { - gameCode: string; - playerName: string; - reason?: string; - message: string; - timestamp: string; -}); - -// Server → All Players: Player was approved -socket.on('game:player-approved', { - playerName: string; - playerId?: string; - message: string; - timestamp: string; -}); - -// Client → Server: Join after approval (private games) -socket.emit('game:join-approved', { gameToken: string }); -``` - -#### Game State Events - -```typescript -// Server → Individual Player: Current game state -socket.on('game:state', { - // Complete game state object - gameCode: string; - players: PlayerPosition[]; - currentTurn: number; - currentPlayer: string; - turnSequence: string[]; - // ... additional state data -}); - -// Server → All Players: Game state update -socket.on('game:state-update', { - // Updated game state after action -}); -``` - -#### Chat System - -```typescript -// Client → Server: Send chat message -socket.emit('game:chat', { - gameCode: string; - message: string; -}); - -// Server → All Players: Chat message received -socket.on('game:chat-message', { - playerName: string; - playerId?: string; - message: string; - timestamp: string; -}); -``` - -#### Player Disconnect Events - -```typescript -// Server → All Players: Player disconnected -socket.on('game:player-disconnected', { - playerName: string; - playerId?: string; - message: string; - timestamp: string; -}); - -// Server → All Players: Player disconnected during card answer -socket.on('game:player-disconnected-during-card', { - playerName: string; - playerId: string; - message: string; - timestamp: string; -}); - -// Client → Server: Leave game voluntarily -socket.emit('game:leave', { gameCode: string }); - -// Server → All Players: Player left game -socket.on('game:player-left', { - playerName: string; - playerId?: string; - message: string; - playerCount: number; - timestamp: string; -}); -``` - -#### Game Start Notification - -```typescript -// Server → All Players: Game started (after REST /start call) -socket.on('game:started', { - message: string; - boardData: BoardData; - turnSequence: string[]; // Array of player IDs in turn order - currentPlayer: string; // First player ID - currentPlayerName: string; - timestamp: string; -}); - -// Server → Current Player: Your turn notification -socket.on('game:your-turn', { - message: string; - canRoll: boolean; - isExtraTurn?: boolean; // True if using extra turn - timestamp: string; -}); -``` - ---- - -### Gameplay Events - -#### Dice Roll - -```typescript -// Client → Server: Roll dice -socket.emit('game:dice-roll', { - gameCode: string; - diceValue: number; // 1-6 -}); - -// Server → All Players: Dice rolled -socket.on('game:dice-rolled', { - playerId: string; - playerName: string; - diceValue: number; - timestamp: string; -}); - -// Server → All Players: Player moved -socket.on('game:player-moved', { - playerId: string; - playerName: string; - oldPosition: number; - newPosition: number; - diceValue: number; - timestamp: string; -}); -``` - ---- - -#### Card Drawing & Question Cards - -```typescript -// Server → All Players: Card drawn (everyone sees question) -socket.on('game:card-drawn', { - playerName: string; - playerId: string; - cardType: string; // "QUIZ", "SENTENCE_PAIRING", etc. - question: string; // The question text - fieldType: string; // "positive" | "negative" | "luck" - timestamp: string; -}); - -// Server → Drawing Player: Interactive card details -socket.on('game:card-drawn-self', { - cardData: { - cardid: string; - question: string; - type: CardType; - timeLimit: number; // 60 seconds - answerOptions?: QuizOption[]; // For QUIZ (multiple choice) - sentencePairs?: SentencePair[]; // For SENTENCE_PAIRING (left-right matching) - words?: string[]; // For SENTENCE_PAIRING (legacy scrambled words) - // ... type-specific data - }; - timestamp: string; -}); - -// SENTENCE_PAIRING Data Structures - -/** - * Sentence pair for left-right matching - */ -interface SentencePair { - id: string; // Unique identifier (e.g., "pair_0", "pair_1") - left: string; // Left part to match (shown in order) - right: string; // Right part (scrambled position) -} - -/** - * Player's answer for sentence pairing - */ -interface SentencePairingAnswer { - pairId: string; // ID of the pair being matched - leftText: string; // Left part text - rightText: string; // Player's chosen right part -} - -// Example: Matching fruits to colors -// Card data sent to player: -{ - sentencePairs: [ - { id: "pair_0", left: "Apple", right: "Yellow" }, // Right parts - { id: "pair_1", left: "Banana", right: "Orange" }, // are scrambled - { id: "pair_2", left: "Orange", right: "Red" } - ] -} - -// Player's answer: -{ - answer: [ - { pairId: "pair_0", leftText: "Apple", rightText: "Red" }, - { pairId: "pair_1", leftText: "Banana", rightText: "Yellow" }, - { pairId: "pair_2", leftText: "Orange", rightText: "Orange" } - ] -} - -// Client → Server: Submit answer -socket.emit('game:card-answer', { - gameCode: string; - answer: any; // Type depends on card type: - // - QUIZ: string (A/B/C/D) - // - SENTENCE_PAIRING: SentencePairingAnswer[] or string (legacy) - // - OWN_ANSWER: string - // - TRUE_FALSE: boolean or "true"/"false" - // - CLOSER: number -}); - -// Server → All Players: Answer submitted (before validation) -socket.on('game:answer-submitted', { - playerName: string; - playerId: string; - answer: any; - message: string; - timestamp: string; -}); - -// Server → All Players: Answer validated -socket.on('game:answer-validated', { - playerName: string; - playerId: string; - isCorrect: boolean; - correctAnswer: any; - message: string; - timestamp: string; -}); -``` - ---- - -#### Position Guessing (Question Cards) - -```typescript -// Server → Player: Request position guess -socket.on('game:position-guess-request', { - message: string; - currentPosition: number; // Starting position - diceRoll: number; // Original dice value - fieldStepValue: number; // Field's step value - patternModifier: number; // Zone-based modifier - timeLimit: number; // 30 seconds - timestamp: string; -}); - -// Client → Server: Submit position guess -socket.emit('game:position-guess', { - gameCode: string; - guessedPosition: number; -}); - -// Server → All Players: Player is guessing (notification) -socket.on('game:player-guessing', { - playerId: string; - playerName: string; - message: string; - timestamp: string; -}); - -// Server → All Players: Player's guess broadcast -socket.on('game:position-guess-broadcast', { - playerId: string; - playerName: string; - guessedPosition: number; - message: string; - timestamp: string; -}); - -// Server → All Players: Guess result with full calculation -socket.on('game:guess-result', { - playerId: string; - playerName: string; - guessedPosition: number; - actualPosition: number; // Calculated position - finalPosition: number; // After penalty (if wrong) - guessCorrect: boolean; - penaltyApplied: boolean; // -2 if wrong - calculation: { - startPosition: number; - diceRoll: number; - stepValue: number; - patternModifier: number; - calculatedPosition: number; - penalty: number; // 0 or -2 - }; - message: string; - timestamp: string; -}); - -// Server → All Players: Player didn't move -socket.on('game:no-movement', { - playerId: string; - playerName: string; - reason: string; // "Wrong answer on positive field" - message: string; - timestamp: string; -}); - -// Server → All Players: Penalty avoided -socket.on('game:penalty-avoided', { - playerId: string; - playerName: string; - message: string; // "Avoided penalty on negative field" - timestamp: string; -}); -``` - ---- - -#### Luck Cards - -```typescript -// Server → All Players: Luck card consequence applied -socket.on('game:luck-consequence', { - playerId: string; - playerName: string; - consequenceType: string; // "MOVE_FORWARD" | "MOVE_BACKWARD" | etc. - value: number; // Magnitude of effect - newPosition?: number; // For movement consequences - turnsToLose?: number; // For LOSE_TURN - extraTurns?: number; // For EXTRA_TURN - message: string; - timestamp: string; -}); -``` - -**Consequence Types**: -- `MOVE_FORWARD` (0): Move forward X steps -- `MOVE_BACKWARD` (1): Move backward X steps -- `LOSE_TURN` (2): Lose X turns -- `EXTRA_TURN` (3): Gain X extra turns -- `GO_TO_START` (5): Return to position 1 - ---- - -#### Joker Cards & Gamemaster Decisions - -```typescript -// Server → All Players: Joker card drawn -socket.on('game:joker-drawn', { - playerName: string; - playerId: string; - jokerCard: { - question: string; - consequence: Consequence; - }; - waitingForGamemaster: boolean; - timestamp: string; -}); - -// Server → Gamemaster: Decision request -socket.on('game:gamemaster-decision-request', { - requestId: string; - playerName: string; - playerId: string; - jokerCard: { - question: string; - consequence: Consequence; - }; - timeLimit: number; // 120 seconds - timestamp: string; -}); - -// Client → Server: Gamemaster decision -socket.emit('game:gamemaster-decision', { - gameCode: string; - requestId: string; - decision: 'approve' | 'reject'; -}); - -// Server → All Players: Decision result -socket.on('game:gamemaster-decision-result', { - playerName: string; - playerId: string; - gamemasterName: string; - decision: string; // "approve" | "reject" - approved: boolean; - consequence: number | null; - description: string; - timestamp: string; -}); - -// Server → Player: Joker position guess request -socket.on('game:joker-position-guess-request', { - message: string; - currentPosition: number; - diceRoll: number; // Always 6 for jokers - fieldStepValue: number; - patternModifier: number; - timeLimit: number; // 30 seconds - timestamp: string; -}); - -// Client → Server: Joker position guess -socket.emit('game:joker-position-guess', { - gameCode: string; - guessedPosition: number; -}); - -// Server → All Players: Joker card complete -socket.on('game:joker-complete', { - playerId: string; - playerName: string; - guessedPosition: number; - actualPosition: number; - finalPosition: number; // Current position if didn't move - guessCorrect: boolean; - penaltyApplied: boolean; - moved: boolean; // Based on field type + decision - calculation: { - startPosition: number; - diceRoll: number; // 6 - stepValue: number; - patternModifier: number; - calculatedPosition: number; - penalty: number; - }; - message: string; - timestamp: string; -}); -``` - -**Joker Flow Logic**: -- **Positive field + approved**: Player moves (with guess) -- **Positive field + rejected**: Player doesn't move -- **Negative field + approved**: Player doesn't move (avoided penalty) -- **Negative field + rejected**: Player moves (with guess - penalty) - ---- - -#### Turn Advancement & Turn Tracking - -```typescript -// Server → All Players: Turn changed -socket.on('game:turn-changed', { - currentPlayer: string; // Player ID - currentPlayerName: string; - turnNumber: number; - message: string; - timestamp: string; -}); - -// Server → All Players: Extra turn remaining -socket.on('game:extra-turn-remaining', { - playerId: string; - playerName: string; - remainingExtraTurns: number; // How many left after this one - message: string; - timestamp: string; -}); - -// Server → All Players: Players skipped -socket.on('game:players-skipped', { - skippedPlayers: Array<{ - playerId: string; - playerName: string; - remainingTurnsToLose: number; - }>; - message: string; - timestamp: string; -}); -``` - ---- - -#### Game End & Cleanup - -```typescript -// Server → All Players: Game ended -socket.on('game:ended', { - winner: string; // Player ID - winnerName: string; - message: string; - finalPositions: PlayerPosition[]; - timestamp: string; - reason?: string; // Optional: 'gamemaster_left' if GM disconnected - gamemasterName?: string; // Optional: GM name if GM left -}); - -// Server → All Players: Cleanup complete -socket.on('game:cleanup-complete', { - gameCode: string; - message: string; - timestamp: string; -}); -``` - -**Game End Scenarios**: - -1. **Normal Win** (Player reaches position 100): - ```typescript - { - winner: "player-uuid", - winnerName: "Alice", - message: "🎉 Alice won the game! Congratulations!", - finalPositions: [...], - timestamp: "2025-11-06T..." - } - ``` - -2. **Gamemaster Disconnect** (Game cancelled): - ```typescript - { - reason: "gamemaster_left", - gamemasterName: "Bob", - message: "🎭 Gamemaster Bob left. Game has ended.", - timestamp: "2025-11-06T..." - } - ``` - - **Behavior**: - - Game immediately cancelled when gamemaster disconnects - - All players notified via `game:ended` event - - Database updated: `state = CANCELLED`, `enddate = now` - - All Redis data and socket connections cleaned up - - No winner recorded (game didn't complete normally) - ---- - -#### Error Events - -```typescript -// Server → Client: Error occurred -socket.on('game:error', { - message: string; - code?: string; - timestamp: string; -}); - -// Server → All Players: Card error (no cards available) -socket.on('game:card-error', { - playerName: string; - playerId: string; - error: string; - timestamp: string; -}); - -// Server → All Players: Joker error -socket.on('game:joker-error', { - playerName: string; - playerId: string; - error: string; - timestamp: string; -}); -``` - ---- - -## Interfaces & Data Structures - -### WebSocket Interfaces - -Located in: `src/Application/Services/Interfaces/GameInterfaces.ts` - -```typescript -/** - * Join game room data - */ -export interface JoinGameData { - gameToken: string; // JWT token from REST /join endpoint -} - -/** - * Leave game data - */ -export interface LeaveGameData { - gameCode: string; -} - -/** - * Dice roll data - */ -export interface DiceRollData { - gameCode: string; - diceValue: number; // 1-6 -} - -/** - * Player position tracking - */ -export interface PlayerPosition { - playerId: string; - playerName: string; - boardPosition: number; - turnOrder: number; -} - -/** - * Game chat message - */ -export interface GameChatData { - gameCode: string; - message: string; -} - -/** - * Card answer data - */ -export interface CardAnswerData { - gameCode: string; - answer: any; // Type depends on card type -} - -/** - * Gamemaster decision data - */ -export interface GamemasterDecisionData { - gameCode: string; - requestId: string; - decision: 'approve' | 'reject'; -} - -/** - * Field effect calculation request - */ -export interface FieldEffectRequest { - gameId: string; - playerId: string; - playerName: string; - currentPosition: number; - card: GameCard; - field: GameField; - dice: number; - guessedPosition?: number; -} - -/** - * Field effect calculation result - */ -export interface FieldEffectResult { - finalPosition: number; - stepValue: number; - dice: number; - patternModifier: number; - consequenceModifier: number; - guessResult?: GuessResult; - gamemasterResult?: GamemasterDecisionResult; - description: string; - effects: string[]; - turnEffect?: TurnEffect; -} - -/** - * Turn effect (for multi-turn tracking) - */ -export interface TurnEffect { - type: 'LOSE_TURN' | 'EXTRA_TURN'; - playerId: string; - value: number; // Number of turns -} - -/** - * Guess result details - */ -export interface GuessResult { - guessedPosition: number; - actualPosition: number; - isCorrect: boolean; - penaltyApplied: boolean; - description: string; -} -``` - ---- - -### Pending State Interfaces - -Located in: `src/Application/Services/GameWebSocketService.ts` - -```typescript -/** - * Pending card state (stored in Redis) - * Used for question cards requiring answers - */ -interface PendingCardState { - playerId: string; - playerName: string; - card: GameCard; - field: GameField; // Field where card was drawn - dice: number; // Original dice roll - currentPosition: number; // Position before card effect - drawnAt: number; // Timestamp - answerGiven?: boolean; // Has player answered? - answerCorrect?: boolean; // Was answer correct? - requiresGuess?: boolean; // Does this require position guess? - guessedPosition?: number; // Player's guessed position -} - -/** - * Pending gamemaster decision state (stored in Redis) - * Used for joker cards - */ -interface PendingDecisionState { - playerId: string; - playerName: string; - card: GameCard; - field: GameField; // Field where joker was drawn - dice: number; // Always 6 for jokers - currentPosition: number; // Position before joker effect - drawnAt: number; // Timestamp - recursionDepth: number; // Prevent infinite secondary landings - gamemasterDecided?: boolean; // Has gamemaster decided? - gamemasterApproved?: boolean; // Was it approved? - guessedPosition?: number; // Player's guessed position (if required) -} -``` - -**Redis Keys**: -``` -pending_card:{gameCode}:{playerId} → PendingCardState (TTL: 60s) -pending_decision:{gameCode}:{requestId} → PendingDecisionState (TTL: 120s) -player_extra_turns:{gameCode}:{playerId} → number (extra turns remaining) -player_turns_to_lose:{gameCode}:{playerId} → number (turns to skip) -``` - ---- - -## Domain Aggregates - -### GameAggregate - -Located in: `src/Domain/Game/GameAggregate.ts` - -```typescript -/** - * Main game entity - */ -@Entity('Games') -export class GameAggregate { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'varchar', length: 10, unique: true }) - gamecode: string; // 6-character unique code - - @Column({ type: 'int' }) - maxplayers: number; - - @Column({ type: 'int', default: LoginType.PUBLIC }) - logintype: LoginType; // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION - - @Column({ type: 'int', default: 50 }) - boardsize: number; - - @Column({ type: 'uuid', nullable: false, name: 'createdBy' }) - createdby: string; // Gamemaster user ID - - @Column({ type: 'uuid', nullable: true, name: 'organizationid' }) - orgid: string | null; - - @Column({ type: 'jsonb', default: () => "'[]'", name: 'decks' }) - gamedecks: GameDeck[]; // Decks with cards - - @Column({ type: 'simple-array', default: '' }) - players: string[]; // Array of player IDs/names - - @Column({ type: 'timestamp', nullable: true }) - startdate: Date | null; - - @Column({ type: 'timestamp', nullable: true }) - enddate: Date | null; - - @Column({ type: 'boolean', default: false }) - finished: boolean; - - @Column({ type: 'varchar', nullable: true }) - winner: string | null; - - @Column({ type: 'int', default: 100 }) - totaltiles: number; -} - -/** - * Login type enum - */ -export enum LoginType { - PUBLIC = 0, // Anyone can join with playerName - PRIVATE = 1, // Requires authentication - ORGANIZATION = 2 // Requires auth + org membership -} -``` - ---- - -### GameField - -```typescript -/** - * Individual board field/tile - */ -export interface GameField { - position: number; // 0-100 - type: 'regular' | 'positive' | 'negative' | 'luck'; - stepValue?: number; // 0-3, used in movement calculation -} - -/** - * Complete board data - */ -export interface BoardData { - gameId?: string; - fields: GameField[]; // 100 fields - generationComplete?: boolean; - generatedAt?: Date; - error?: string; -} -``` - -**Field Type Distribution** (generated by `BoardGenerationService`): -- **Regular**: ~40% (no special card) -- **Positive**: ~25% (reward card) -- **Negative**: ~25% (penalty card) -- **Luck**: ~10% (instant consequence) - -**Important**: Fields have NO consequences. Consequences come only from **cards**. - ---- - -### GameCard - -```typescript -/** - * Card in a deck - */ -export interface GameCard { - cardid: string; - question?: string; - answer?: any; // Can be string, number, object, array - type?: CardType; - consequence?: Consequence | null; // Only for LUCK and JOKER cards - played?: boolean; - playerid?: string; -} - -/** - * Card types - */ -export enum CardType { - QUIZ = 0, // Multiple choice (A/B/C/D) - SENTENCE_PAIRING = 1, // Match left parts to right parts - OWN_ANSWER = 2, // Free text answer - TRUE_FALSE = 3, // Boolean answer - CLOSER = 4, // Closest number answer - JOKER = 5, // Gamemaster decision (secondary landing only) - LUCK = 6 // Instant consequence -} -``` - -**Card Answer Formats by Type:** - -| Card Type | Answer Format in Database | Player Answer Format | Description | -|-----------|---------------------------|----------------------|-------------| -| **QUIZ** | `[{answer:"A", text:"...", correct:true}, ...]` | `"A"` (string) | Multiple choice with 4 options | -| **SENTENCE_PAIRING** | `[{left:"Apple", right:"Red"}, ...]` | `[{pairId, leftText, rightText}, ...]` | Match left to right parts | -| **SENTENCE_PAIRING (legacy)** | `"word1 word2 word3"` (string) | `["word1", "word2", "word3"]` or `"word1 word2 word3"` | Reconstruct scrambled sentence | -| **OWN_ANSWER** | `["answer1", "answer2", ...]` (string[]) | `"player answer"` (string) | Free text with acceptable answers | -| **TRUE_FALSE** | `true` or `false` (boolean) | `true/false` or `"true"/"false"` | True/False question | -| **CLOSER** | `{correct: 42, percent: 10}` (object) | `40` (number) | Closest number within percentage | -| **JOKER** | Not applicable | Not applicable | Gamemaster decides | -| **LUCK** | Not applicable | Not applicable | Instant consequence | - ---- - -### Consequence - -Located in: `src/Domain/Deck/DeckAggregate.ts` - -```typescript -/** - * Card consequence structure - */ -export interface Consequence { - type: ConsequenceType; - value?: number; // Magnitude of effect (default: 1) -} - -/** - * Consequence types - */ -export enum ConsequenceType { - MOVE_FORWARD = 0, // Move forward X steps - MOVE_BACKWARD = 1, // Move backward X steps - LOSE_TURN = 2, // Skip X turns - EXTRA_TURN = 3, // Get X extra turns - GO_TO_START = 5 // Return to position 1 -} -``` - -**Multi-Turn Support**: -- `LOSE_TURN` with `value=3`: Player skips next 3 turns -- `EXTRA_TURN` with `value=2`: Player gets 2 additional turns after current - ---- - -### GameDeck - -```typescript -/** - * Deck of cards - */ -export interface GameDeck { - deckid: string; - decktype: DeckType; - cards: GameCard[]; -} - -/** - * Deck types - */ -export enum DeckType { - POSITIVE = 0, // For positive fields - NEGATIVE = 1, // For negative fields - LUCK = 2, // For luck fields - JOKER = 3 // For secondary landings -} -``` - ---- - -## Complete Game Scenarios - -### Scenario 1: Question Card on Positive Field (Correct Answer) - -``` -1. Player at position 20, rolls dice = 4 -2. Field at position 24 has stepValue = 2, type = "positive" -3. Player lands on position 24 -4. Server waits 2 seconds (frontend animation) -5. Server draws POSITIVE deck card (type = QUIZ) -6. Server broadcasts: game:card-drawn (everyone sees question) -7. Server emits: game:card-drawn-self to player (60s timer) -8. Player answers correctly -9. Server broadcasts: game:answer-submitted -10. Server broadcasts: game:answer-validated { isCorrect: true } -11. Server determines: positive field + correct = GUESS REQUIRED -12. Server emits: game:position-guess-request to player (30s timer) - - Shows: currentPosition=20, dice=4, stepValue=2, patternModifier=+2 -13. Player calculates: 20 + 4 + 2 + 2 = 28, guesses 28 -14. Server broadcasts: game:position-guess-broadcast { guessedPosition: 28 } -15. Server calculates: 20 + 4 + 2 + 2 = 28 (correct!) -16. Server broadcasts: game:guess-result { - guessCorrect: true, - finalPosition: 28, - penaltyApplied: false -} -17. Player position updated to 28 -18. Server checks: position 28 is not special field -19. Server calls: advanceTurn() → next player's turn -``` - ---- - -### Scenario 2: Question Card on Negative Field (Wrong Answer) - -``` -1. Player at position 50, rolls dice = 5 -2. Field at position 55 has stepValue = 1, type = "negative" -3. Player lands on position 55 -4. Server waits 2 seconds -5. Server draws NEGATIVE deck card (type = TRUE_FALSE) -6. Server broadcasts: game:card-drawn -7. Server emits: game:card-drawn-self to player -8. Player answers incorrectly -9. Server broadcasts: game:answer-validated { isCorrect: false } -10. Server determines: negative field + wrong = GUESS REQUIRED (penalty test) -11. Server emits: game:position-guess-request - - Shows: currentPosition=50, dice=5, stepValue=1, patternModifier=+1 -12. Player guesses: 57, but actual is: 50 + 5 + 1 + 1 = 57 -13. Server calculates actual: 57, player guessed: 57 (CORRECT!) -14. Server broadcasts: game:guess-result { - guessCorrect: true, - finalPosition: 57, - penaltyApplied: false -} -15. Player moves to 57 (avoided penalty despite wrong answer) -16. Server advances turn -``` - ---- - -### Scenario 3: Luck Card with Multi-Turn EXTRA_TURN - -``` -1. Player at position 30, rolls dice = 3 -2. Field at position 33 has type = "luck" -3. Player lands on position 33 -4. Server draws LUCK deck card -5. Card has: { type: EXTRA_TURN (3), value: 3 } -6. Server broadcasts: game:card-result -7. Server broadcasts: game:luck-consequence { - consequenceType: "EXTRA_TURN", - value: 3, - extraTurns: 3 -} -8. Server calls: setPlayerExtraTurns(gameCode, playerId, 3) - - Redis: player_extra_turns:{gameCode}:{playerId} = 3 -9. Server calls: advanceTurn() -10. advanceTurn() checks: getPlayerExtraTurns() = 3 > 0 -11. Server emits: game:extra-turn-remaining { remainingExtraTurns: 2 } -12. Same player continues (no turn advance) -13. Player rolls again... -14. After turn ends, advanceTurn() checks again: extraTurns = 2 > 0 -15. Process repeats until extraTurns = 0 -16. Player gets 4 total turns (1 original + 3 extra) -``` - ---- - -### Scenario 4: Luck Card with Multi-Turn LOSE_TURN - -``` -1. Player A draws luck card: { type: LOSE_TURN (2), value: 2 } -2. Server calls: setPlayerTurnsToLose(gameCode, playerA, 2) - - Redis: player_turns_to_lose:{gameCode}:{playerA} = 2 -3. Server broadcasts: game:luck-consequence { - consequenceType: "LOSE_TURN", - turnsToLose: 2 -} -4. Server calls: advanceTurn() → skip to Player B - ---- Later, when it's Player A's turn in sequence --- - -5. advanceTurn() reaches Player A in turn sequence -6. Checks: getPlayerTurnsToLose(playerA) = 2 > 0 -7. Decrements: 2 → 1 -8. Adds to skippedPlayers array -9. Continues to next player in sequence (Player B) -10. Server broadcasts: game:players-skipped { - skippedPlayers: [{ - playerName: "Player A", - remainingTurnsToLose: 1 - }] -} -11. Server broadcasts: game:turn-changed { currentPlayer: "Player B" } - ---- Next round --- - -12. advanceTurn() reaches Player A again -13. Checks: getPlayerTurnsToLose(playerA) = 1 > 0 -14. Decrements: 1 → 0, deletes Redis key -15. Skips Player A again -16. Player A skipped 2 times total -``` - ---- - -### Scenario 5: Joker Card on Positive Field (Approved by Gamemaster) - -``` -1. Player completes question card, lands on position 70 (positive field) -2. Server checks secondary landing -3. Server draws JOKER deck card -4. Server broadcasts: game:joker-drawn { waitingForGamemaster: true } -5. Server emits to gamemaster: game:gamemaster-decision-request (120s timer) -6. Gamemaster approves -7. Server broadcasts: game:gamemaster-decision-result { - decision: "approve", - approved: true -} -8. Server determines: positive field + approved = GUESS REQUIRED -9. Server emits: game:joker-position-guess-request - - Shows: currentPosition=70, dice=6, stepValue=1, patternModifier=-2 -10. Player guesses: 75, actual: 70 + 6 + 1 - 2 = 75 (CORRECT!) -11. Server broadcasts: game:joker-complete { - guessCorrect: true, - moved: true, - finalPosition: 75 -} -12. Player moves to 75 -13. Server checks for tertiary landing (not allowed - recursion depth) -14. Server advances turn -``` - ---- - -### Scenario 6: Multiple Players with Turn Tracking - -``` -Turn Sequence: [PlayerA, PlayerB, PlayerC] - ---- Turn 1 --- -PlayerA: Gets EXTRA_TURN (value=2) -- Redis: player_extra_turns:game:PlayerA = 2 -- PlayerA plays again - ---- Turn 2 (PlayerA extra turn 1) --- -PlayerA: Gets LOSE_TURN (value=1) -- Redis: player_turns_to_lose:game:PlayerA = 1 -- Redis: player_extra_turns:game:PlayerA = 1 (still has 1 left) -- advanceTurn() checks: extraTurns = 1 > 0 -- PlayerA plays AGAIN (extra turn takes priority) - ---- Turn 3 (PlayerA extra turn 2) --- -PlayerA: Normal turn -- Redis: player_extra_turns:game:PlayerA = 0 (deleted) -- advanceTurn() → next player - ---- Turn 4 --- -Sequence reaches PlayerA -- Checks: turnsToLose = 1 > 0 -- Skip PlayerA, decrement to 0 -- Server broadcasts: game:players-skipped -- PlayerB plays - ---- Turn 5 --- -PlayerB: Normal turn - ---- Turn 6 --- -PlayerC: Normal turn - ---- Turn 7 --- -Back to PlayerA (turnsToLose = 0) -- PlayerA plays normally -``` - ---- - -## Turn Tracking System - -### Redis Storage - -```typescript -// Extra turns storage -player_extra_turns:{gameCode}:{playerId} → "3" // Number as string -TTL: None (cleared on cleanup or use) - -// Turns to lose storage -player_turns_to_lose:{gameCode}:{playerId} → "2" // Number as string -TTL: None (cleared on cleanup or use) -``` - -### Turn Tracking Methods - -```typescript -class GameWebSocketService { - // Extra turns - private async setPlayerExtraTurns( - gameCode: string, - playerId: string, - count: number - ): Promise; - - private async getPlayerExtraTurns( - gameCode: string, - playerId: string - ): Promise; // Returns 0 if not found - - private async decrementPlayerExtraTurns( - gameCode: string, - playerId: string - ): Promise; // Decrements or deletes if reaches 0 - - // Turns to lose - private async setPlayerTurnsToLose( - gameCode: string, - playerId: string, - count: number - ): Promise; - - private async getPlayerTurnsToLose( - gameCode: string, - playerId: string - ): Promise; // Returns 0 if not found - - private async decrementPlayerTurnsToLose( - gameCode: string, - playerId: string - ): Promise; // Decrements or deletes if reaches 0 - - // Cleanup - private async clearPlayerTurnData( - gameCode: string, - playerId: string - ): Promise; // Deletes both keys -} -``` - -### Enhanced advanceTurn() Logic - -```typescript -private async advanceTurn(gameCode: string): Promise { - // PHASE 1: Check if current player has extra turns - const extraTurns = await this.getPlayerExtraTurns(gameCode, currentPlayerId); - if (extraTurns > 0) { - await this.decrementPlayerExtraTurns(gameCode, currentPlayerId); - emit('game:extra-turn-remaining', { remainingExtraTurns: extraTurns - 1 }); - emit('game:your-turn', { isExtraTurn: true }); - return; // Same player continues - } - - // PHASE 2: Find next player, skipping those with lost turns - let nextTurnIndex = (currentTurnIndex + 1) % turnSequence.length; - const skippedPlayers = []; - let loopGuard = 0; - - while (loopGuard < turnSequence.length) { - const candidatePlayerId = turnSequence[nextTurnIndex]; - const turnsToLose = await this.getPlayerTurnsToLose(gameCode, candidatePlayerId); - - if (turnsToLose > 0) { - await this.decrementPlayerTurnsToLose(gameCode, candidatePlayerId); - skippedPlayers.push({ - playerId: candidatePlayerId, - remainingTurnsToLose: turnsToLose - 1 - }); - nextTurnIndex = (nextTurnIndex + 1) % turnSequence.length; - loopGuard++; - } else { - break; // Found valid player - } - } - - // PHASE 3: Update game state - gameState.currentTurn = nextTurnIndex; - gameState.currentPlayer = turnSequence[nextTurnIndex]; - - // PHASE 4: Notify about skipped players - if (skippedPlayers.length > 0) { - emit('game:players-skipped', { skippedPlayers }); - } - - // PHASE 5: Notify about turn change - emit('game:turn-changed', { currentPlayer, currentPlayerName }); -} -``` - ---- - -## Position Guessing Mechanic - -### Pattern-Based Movement Calculation - -**Formula**: -``` -finalPosition = currentPosition + (stepValue × dice) + patternModifier -``` - -**Pattern Modifiers by Position & Field Type**: -```typescript -private getPatternModifier(position: number, positiveField: boolean): number { - // Dynamic pattern-based modifiers for engaging gameplay - // Sign depends on field type: positive field = positive modifier, negative field = negative modifier - - if (position % 10 === 0) { - return 0; // Positions ending in 0 (10, 20, 30...) - No modifier - } else if (position % 10 === 5) { - return positiveField ? 3 : -3; // Positions ending in 5 (15, 25, 35...) - ±3 modifier - } else if (position % 3 === 0) { - return positiveField ? 2 : -2; // Positions divisible by 3 (9, 12, 21...) - ±2 modifier - } else if (position % 2 === 1) { - return positiveField ? 1 : -1; // Odd positions (1, 7, 11...) - ±1 modifier - } else { - return 0; // Other even positions - No modifier - } -} -``` - -**How Field Type is Determined**: -- `positiveField = true` when `stepValue > 0` (positive field) -- `positiveField = false` when `stepValue < 0` (negative field) - -**Why This Design**: -- **Dynamic**: Every position has different calculation rules based on patterns -- **Learnable**: Players can recognize patterns (ends in 5, divisible by 3, etc.) -- **Field-Dependent**: Positive fields give positive modifiers, negative fields give negative modifiers -- **Skill-Based**: Requires mental calculation and pattern recognition under time pressure (30s) -- **Not Trivial**: Information is available but requires active processing - -### Guess Requirement Logic - -```typescript -private determineGuessRequirement( - fieldType: 'regular' | 'positive' | 'negative' | 'luck', - answerCorrect: boolean -): boolean { - if (fieldType === 'positive') { - return answerCorrect; // Correct = guess for REWARD - } else if (fieldType === 'negative') { - return !answerCorrect; // Wrong = guess for PENALTY - } - return false; // Regular and luck fields never require guess -} -``` - -### Question Card Guess Flow - -**Matrix**: -| Field Type | Answer | Guess Required? | Reason | -|------------|---------|-----------------|---------------------------| -| Positive | Correct | **YES** | Reward scenario | -| Positive | Wrong | NO | No movement | -| Negative | Correct | NO | Avoided penalty | -| Negative | Wrong | **YES** | Penalty test | -| Regular | Any | NO | No special fields | -| Luck | N/A | NO | Instant consequence | - -### Joker Card Guess Flow - -**Matrix** (dice always = 6): -| Field Type | Gamemaster | Guess Required? | Movement | -|------------|------------|-----------------|---------------------| -| Positive | Approved | **YES** | Move if guess | -| Positive | Rejected | NO | No movement | -| Negative | Approved | NO | No movement (avoid) | -| Negative | Rejected | **YES** | Move if guess | - -### Penalty System - -- **Wrong guess**: -2 steps from calculated position -- **Minimum position**: 1 (can't go below start) -- **Applied after calculation**: `finalPosition = max(1, calculatedPosition - 2)` - -### Calculation Examples - -**Example 1**: Position 15 (ends in 5), positive field, dice 4, stepValue 2 -``` -positiveField = true (stepValue 2 > 0) -patternModifier = 3 (position ends in 5, positive field) -calculation = 15 + (2 × 4) + 3 = 15 + 8 + 3 = 26 -``` - -**Example 2**: Position 35 (ends in 5), negative field, dice 6, stepValue -1 -``` -positiveField = false (stepValue -1 < 0) -patternModifier = -3 (position ends in 5, negative field) -calculation = 35 + (-1 × 6) + (-3) = 35 - 6 - 3 = 26 -``` - -**Example 3**: Position 21 (divisible by 3), positive field, dice 5, stepValue 2 -``` -positiveField = true (stepValue 2 > 0) -patternModifier = 2 (position divisible by 3, positive field) -calculation = 21 + (2 × 5) + 2 = 21 + 10 + 2 = 33 -``` - -**Example 4**: Position 20 (ends in 0), any field type, dice 4, stepValue 2 -``` -patternModifier = 0 (position ends in 0, always 0) -calculation = 20 + (2 × 4) + 0 = 20 + 8 = 28 -``` - -**Example 5**: Position 7 (odd), negative field, dice 3, stepValue -2 -``` -positiveField = false (stepValue -2 < 0) -patternModifier = -1 (position is odd, negative field) -calculation = 7 + (-2 × 3) + (-1) = 7 - 6 - 1 = 0 → clamped to 1 -``` - ---- - -## Error Handling - -### REST API Error Responses - -**Format**: -```typescript -{ - error: string; // Human-readable error message - code?: string; // Optional error code -} -``` - -**Status Codes**: -- `400`: Bad Request (validation errors, missing fields) -- `401`: Unauthorized (authentication required) -- `403`: Forbidden (insufficient permissions, org mismatch) -- `404`: Not Found (game, deck, user not found) -- `409`: Conflict (game full, already started, player already in game) -- `500`: Internal Server Error - -### WebSocket Error Events - -```typescript -socket.on('game:error', { - message: string; - code?: string; - timestamp: string; -}); -``` - -**Common Errors**: -- Authentication failed -- Invalid game code -- Game already started -- Not your turn -- Invalid answer format -- Timeout expired -- No cards available -- Internal server error - -### Timeout Handling - -**Card Answer Timeout** (60 seconds): -```typescript -// Server auto-processes as wrong answer -socket.on('game:card-timeout', { - playerName: string; - message: string; - timestamp: string; -}); -``` - -**Gamemaster Decision Timeout** (120 seconds): -```typescript -// Server auto-rejects joker card -socket.on('game:gamemaster-timeout', { - requestId: string; - playerName: string; - message: string; - timestamp: string; -}); -``` - -**Position Guess Timeout** (30 seconds): -```typescript -// Server treats as no movement -socket.on('game:guess-timeout', { - playerName: string; - message: string; - timestamp: string; -}); -``` - -### Redis Cleanup - -**Automatic Cleanup**: -- Game end: All keys deleted -- Player disconnect: Pending states cleared -- Timeout: Keys expire naturally (TTL) - -**Manual Cleanup**: -```typescript -// Called on game:ended or disconnect -await cleanupGameData(gameCode, gameId); -``` - -**Keys Cleaned**: -``` -gameplay:{gameCode} -game_state:{gameCode} -game_board_{gameCode} -game_connections:{gameCode} -game_ready:{gameCode} -game_pending:{gameCode} -game_positions:{gameCode} -pending_card:{gameCode}:{playerId} -pending_decision:{gameCode}:{requestId} -player_extra_turns:{gameCode}:{playerId} -player_turns_to_lose:{gameCode}:{playerId} -``` - ---- - -## Card Type Implementation Details - -### SENTENCE_PAIRING Card Type - -**Purpose**: Test player's ability to match related items (left parts to right parts) - -**Use Cases**: -- Match words to definitions -- Match questions to answers -- Match countries to capitals -- Match names to descriptions -- Match terms to translations - -#### Database Format (NEW) - -```typescript -// In DeckAggregate Card.answer field -{ - text: "Match each fruit to its color", - type: CardType.SENTENCE_PAIRING, // 1 - answer: [ - { left: "Apple", right: "Red" }, - { left: "Banana", right: "Yellow" }, - { left: "Orange", right: "Orange" }, - { left: "Grape", right: "Purple" } - ] -} -``` - -#### Client Preparation - -Server scrambles the **right parts** while keeping left parts in order: - -```typescript -// Sent to client in game:card-drawn-self -{ - cardData: { - question: "Match each fruit to its color", - type: 1, // SENTENCE_PAIRING - sentencePairs: [ - { id: "pair_0", left: "Apple", right: "Purple" }, // Scrambled! - { id: "pair_1", left: "Banana", right: "Orange" }, - { id: "pair_2", left: "Orange", right: "Red" }, - { id: "pair_3", left: "Grape", right: "Yellow" } - ], - timeLimit: 60 - } -} -``` - -#### Player Answer Format - -```typescript -// Player submits array of matches -{ - answer: [ - { pairId: "pair_0", leftText: "Apple", rightText: "Red" }, - { pairId: "pair_1", leftText: "Banana", rightText: "Yellow" }, - { pairId: "pair_2", leftText: "Orange", rightText: "Orange" }, - { pairId: "pair_3", leftText: "Grape", rightText: "Purple" } - ] -} -``` - -#### Validation Logic - -```typescript -// For each correct pair in database: -1. Find player's match for the left part -2. Compare player's chosen right part to correct right part -3. Case-insensitive comparison with trimmed whitespace -4. ALL pairs must match correctly for answer to be correct - -// Example validation result: -{ - isCorrect: true, // Only if ALL pairs match - submittedAnswer: [...], - correctAnswer: [...], - explanation: "✅ Perfect! All 4 pairs matched correctly! - ✓ \"Apple\" → \"Red\" - ✓ \"Banana\" → \"Yellow\" - ✓ \"Orange\" → \"Orange\" - ✓ \"Grape\" → \"Purple\"" -} -``` - -#### Partial Match Example - -```typescript -// Player gets 2 out of 4 correct: -{ - isCorrect: false, - explanation: "❌ Only 2/4 pairs correct: - ✓ \"Apple\" → \"Red\" - ✗ \"Banana\" → \"Purple\" (should be \"Yellow\") - ✗ \"Orange\" → \"Yellow\" (should be \"Orange\") - ✓ \"Grape\" → \"Purple\"" -} -``` - -#### Legacy Format (Backward Compatibility) - -**Old Database Format**: -```typescript -{ - text: "The quick brown fox jumps over the lazy dog", - type: CardType.SENTENCE_PAIRING, - answer: "The quick brown fox jumps over the lazy dog" // String -} -``` - -**Client Preparation (Legacy)**: -```typescript -{ - cardData: { - question: "Arrange the words to form a sentence:", - type: 1, - words: ["lazy", "The", "dog", "fox", "over", "quick", "brown", "jumps", "the"], - timeLimit: 60 - } -} -``` - -**Player Answer (Legacy)**: -```typescript -{ - answer: ["The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"] - // OR - answer: "The quick brown fox jumps over the lazy dog" -} -``` - -#### Frontend Implementation Example - -```typescript -// React/Vue component for SENTENCE_PAIRING -interface Props { - sentencePairs: SentencePair[]; - onSubmit: (answer: SentencePairingAnswer[]) => void; -} - -function SentencePairingCard({ sentencePairs, onSubmit }: Props) { - const [matches, setMatches] = useState>(new Map()); - - const handleMatch = (pairId: string, leftText: string, rightText: string) => { - setMatches(prev => new Map(prev).set(pairId, rightText)); - }; - - const handleSubmit = () => { - const answer: SentencePairingAnswer[] = sentencePairs.map(pair => ({ - pairId: pair.id, - leftText: pair.left, - rightText: matches.get(pair.id) || '' - })); - onSubmit(answer); - }; - - return ( -
- {sentencePairs.map(pair => ( -
- {pair.left} - -
- ))} - -
- ); -} -``` - -#### Database Migration - -**No migration needed!** The implementation supports both formats: - -1. **New cards**: Use array of `{left, right}` objects -2. **Existing cards**: Continue working with string format -3. Detection is automatic based on `answer` field type - -**Creating New Sentence Pairing Cards**: -```sql --- Example INSERT for new format -INSERT INTO "Cards" (deck_id, text, type, answer) -VALUES ( - 'deck-uuid', - 'Match programming languages to their creators', - 1, -- SENTENCE_PAIRING - '[ - {"left": "Python", "right": "Guido van Rossum"}, - {"left": "JavaScript", "right": "Brendan Eich"}, - {"left": "C++", "right": "Bjarne Stroustrup"}, - {"left": "Ruby", "right": "Yukihiro Matsumoto"} - ]'::jsonb -); -``` - ---- - -## Appendix: Complete Event Summary - -### Client → Server Events - -| Event | Data | Description | -|-------|------|-------------| -| `game:join` | `{ gameToken: string }` | Join game room with auth token | -| `game:leave` | `{ gameCode: string }` | Leave game voluntarily | -| `game:ready` | `{ gameCode: string, ready: boolean }` | Mark player as ready/not ready | -| `game:approve-player` | `{ gameCode: string, playerName: string }` | Gamemaster approves player (PRIVATE) | -| `game:reject-player` | `{ gameCode: string, playerName: string, reason?: string }` | Gamemaster rejects player (PRIVATE) | -| `game:join-approved` | `{ gameToken: string }` | Join after approval (PRIVATE) | -| `game:chat` | `{ gameCode: string, message: string }` | Send chat message | -| `game:action` | `{ gameCode: string, action: string, data?: any }` | Generic game action | -| `game:dice-roll` | `{ gameCode: string, diceValue: number }` | Roll dice (1-6) | -| `game:card-answer` | `{ gameCode: string, answer: any }` | Submit card answer | -| `game:gamemaster-decision` | `{ gameCode: string, requestId: string, decision: string }` | Gamemaster decision on joker | -| `game:position-guess` | `{ gameCode: string, guessedPosition: number }` | Submit position guess (question) | -| `game:joker-position-guess` | `{ gameCode: string, guessedPosition: number }` | Submit position guess (joker) | - -### Server → Client Events - -| Event | Audience | Description | -|-------|----------|-------------| -| `game:joined` | Individual | Successful join, joined rooms | -| `game:state` | Individual | Current game state sent | -| `game:pending-approval` | Individual | Waiting for gamemaster approval (PRIVATE) | -| `game:approval-granted` | Individual | Join request approved (PRIVATE) | -| `game:approval-denied` | Individual | Join request rejected (PRIVATE) | -| `game:player-joined` | All | Player joined game | -| `game:player-left` | All | Player left game | -| `game:player-disconnected` | All | Player disconnected unexpectedly | -| `game:player-disconnected-during-card` | All | Player disconnected during card answer | -| `game:player-requesting-join` | Gamemaster | Player wants to join (PRIVATE) | -| `game:player-approved` | All | Player was approved by gamemaster | -| `game:player-ready` | All | Player ready status changed | -| `game:all-ready` | All | All players ready | -| `game:chat-message` | All | Chat message from player | -| `game:action-result` | All | Generic action result | -| `game:state-update` | All | Game state updated | -| `game:started` | All | Game started, board generated | -| `game:turn-changed` | All | Turn advanced to next player | -| `game:your-turn` | Individual | Your turn notification | -| `game:dice-rolled` | All | Dice rolled by player | -| `game:player-moved` | All | Player moved to new position | -| `game:card-drawn` | All | Card drawn (question shown) | -| `game:card-drawn-self` | Individual | Interactive card data | -| `game:card-result` | All | Card result (for LUCK cards) | -| `game:card-timeout` | All | Player timed out on card answer | -| `game:answer-submitted` | All | Answer submitted (pre-validation) | -| `game:answer-validated` | All | Answer validation result | -| `game:position-guess-request` | Individual | Request position guess | -| `game:player-guessing` | All | Player is calculating guess | -| `game:position-guess-broadcast` | All | Player's guess shown | -| `game:guess-result` | All | Guess result with calculation | -| `game:no-movement` | All | Player didn't move | -| `game:penalty-avoided` | All | Player avoided penalty | -| `game:luck-consequence` | All | Luck card consequence applied | -| `game:joker-drawn` | All | Joker card drawn | -| `game:gamemaster-decision-request` | Gamemaster | Decision request | -| `game:gamemaster-decision-result` | All | Decision result | -| `game:gamemaster-timeout` | All | Gamemaster timed out on decision | -| `game:joker-position-guess-request` | Individual | Joker position guess request | -| `game:joker-complete` | All | Joker card processing complete | -| `game:joker-error` | All | Joker card error | -| `game:extra-turn-remaining` | All | Player using extra turn | -| `game:players-skipped` | All | Players skipped (lost turns) | -| `game:ended` | All | Game ended, winner declared | -| `game:cleanup-complete` | All | Cleanup finished | -| `game:error` | Individual | Error occurred | -| `game:card-error` | All | Card drawing error | - ---- - -**End of Documentation** \ No newline at end of file diff --git a/Documentations/COMPLETE_GAME_WORKFLOW.pdf b/Documentations/COMPLETE_GAME_WORKFLOW.pdf deleted file mode 100644 index 08e14ee5..00000000 Binary files a/Documentations/COMPLETE_GAME_WORKFLOW.pdf and /dev/null differ diff --git a/Documentations/DATABASE_MANAGEMENT_GUIDE.md b/Documentations/DATABASE_MANAGEMENT_GUIDE.md deleted file mode 100644 index dc6a793b..00000000 --- a/Documentations/DATABASE_MANAGEMENT_GUIDE.md +++ /dev/null @@ -1,392 +0,0 @@ -# 🗄️ SerpentRace Database Management Guide - -## 🎯 Overview - -This guide provides comprehensive information about managing all database services in the SerpentRace project, including PostgreSQL, Redis, MinIO, and administration tools. - -## 📊 Quick Status Check - -### Check All Services -```bash -npm run db:status -``` - -### Check Individual Services -```bash -npm run db:status:pg # PostgreSQL only -npm run db:status:redis # Redis only -npm run db:status:docker # Docker containers only -``` - -### Simple Connection Test -```bash -npm run test:connections -``` - -## 🐘 PostgreSQL Database - -### Connection Details -- **Host**: localhost:5432 -- **Database**: serpentrace -- **Username**: postgres -- **Password**: postgres -- **Admin Tool**: pgAdmin at http://localhost:8080 - -### Database Operations - -#### Run Migrations -```bash -npm run migration:run -``` - -#### Create New Migration -```bash -npm run migration:create src/migrations/YourMigrationName -``` - -#### Generate Migration from Entity Changes -```bash -npm run migration:generate src/migrations/YourMigrationName -``` - -#### Check Migration Status -```bash -npm run migration:show -``` - -#### Rollback Last Migration -```bash -npm run migration:revert -``` - -### Direct Database Access - -#### Using psql (if installed) -```bash -psql -h localhost -p 5432 -U postgres -d serpentrace -``` - -#### Using pgAdmin -1. Open http://localhost:8080 -2. Login with: admin@serpentrace.dev / admin -3. Server should be pre-configured as "SerpentRace" - -### Common SQL Queries - -#### Check Database Size -```sql -SELECT pg_size_pretty(pg_database_size('serpentrace')) as size; -``` - -#### List All Tables -```sql -SELECT tablename FROM pg_tables WHERE schemaname = 'public'; -``` - -#### Check Active Connections -```sql -SELECT count(*) FROM pg_stat_activity WHERE datname = 'serpentrace'; -``` - -## 🔴 Redis Cache - -### Connection Details -- **Host**: localhost:6379 -- **No Authentication**: Default Redis setup -- **Admin Tool**: Redis Commander at http://localhost:8081 - -### Redis Operations - -#### Direct Redis Access (if redis-cli installed) -```bash -redis-cli -h localhost -p 6379 -``` - -#### Common Redis Commands -```bash -# Get all keys -KEYS * - -# Get key count -DBSIZE - -# Check memory usage -INFO memory - -# Flush all data (careful!) -FLUSHALL -``` - -### Using Redis Commander -1. Open http://localhost:8081 -2. Browse keys, view data, execute commands - -## 🗄️ MinIO Object Storage - -### Connection Details -- **Endpoint**: localhost:9000 -- **Console**: http://localhost:9001 -- **Access Key**: serpentrace -- **Secret Key**: serpentrace123 -- **Default Bucket**: serpentrace - -### MinIO Operations - -#### Access MinIO Console -1. Open http://localhost:9001 -2. Login with: serpentrace / serpentrace123 -3. Create buckets, upload files, manage storage - -#### Health Check -```bash -curl http://localhost:9000/minio/health/live -``` - -### File Upload Example (Node.js) -```javascript -const Minio = require('minio'); - -const minioClient = new Minio.Client({ - endPoint: 'localhost', - port: 9000, - useSSL: false, - accessKey: 'serpentrace', - secretKey: 'serpentrace123' -}); - -// Upload file -minioClient.fPutObject('serpentrace', 'test-file.txt', './file.txt'); -``` - -## 🐳 Docker Container Management - -### View All Containers -```bash -docker ps -a -``` - -### View SerpentRace Containers Only -```bash -docker ps -a --filter "name=serpentrace" -``` - -### Container Operations - -#### Restart All Services -```bash -cd d:\munka\SzeSnake\SerpentRace_Docker -docker-compose -f docker-compose.dev.yml restart -``` - -#### Restart Individual Service -```bash -docker restart serpentrace-postgres-dev # PostgreSQL -docker restart serpentrace-redis-dev # Redis -docker restart serpentrace-minio-dev # MinIO -docker restart serpentrace-pgadmin-dev # pgAdmin -``` - -#### View Container Logs -```bash -docker logs serpentrace-postgres-dev -docker logs serpentrace-redis-dev -f # Follow logs -``` - -#### Stop All Services -```bash -cd d:\munka\SzeSnake\SerpentRace_Docker -docker-compose -f docker-compose.dev.yml down -``` - -#### Start All Services -```bash -cd d:\munka\SzeSnake\SerpentRace_Docker -docker-compose -f docker-compose.dev.yml up -d -``` - -## 🛠️ Troubleshooting - -### PostgreSQL Issues - -#### Connection Refused -```bash -# Check if container is running -docker ps | grep postgres - -# Check container logs -docker logs serpentrace-postgres-dev - -# Restart if needed -docker restart serpentrace-postgres-dev -``` - -#### Migration Errors -```bash -# Check migration status -npm run migration:show - -# Revert last migration if problematic -npm run migration:revert - -# Re-run migrations -npm run migration:run -``` - -### Redis Issues - -#### Cannot Connect -```bash -# Check Redis container -docker ps | grep redis - -# Test connection -redis-cli -h localhost -p 6379 ping -# Expected response: PONG -``` - -### MinIO Issues - -#### Health Check Failed -```bash -# Check MinIO container -docker ps | grep minio - -# Test health endpoint -curl http://localhost:9000/minio/health/live -# Expected response: 200 OK -``` - -### pgAdmin Issues - -#### Cannot Login -- Default credentials: admin@serpentrace.dev / admin -- If issues persist, restart container: - ```bash - docker restart serpentrace-pgladmin-dev - ``` - -#### Server Not Found -- pgAdmin should auto-configure the PostgreSQL server -- If not visible, add manually: - - Host: postgres - - Port: 5432 - - Database: serpentrace - - Username: postgres - - Password: postgres - -## 🔧 Environment Variables - -### Default Development Settings -```bash -# PostgreSQL -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=postgres - -# Redis -REDIS_HOST=localhost -REDIS_PORT=6379 - -# MinIO -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_ACCESS_KEY=serpentrace -MINIO_SECRET_KEY=serpentrace123 -``` - -### Production Configuration -Create `.env.production` with secure values: -```bash -DB_HOST=your-production-host -DB_PASSWORD=secure-password -REDIS_PASSWORD=secure-redis-password -MINIO_SECRET_KEY=secure-minio-secret -``` - -## 📈 Monitoring & Maintenance - -### Daily Health Check -```bash -npm run db:status -``` - -### Weekly Maintenance -```bash -# Check database size growth -npm run db:status:pg - -# Review Redis memory usage -npm run db:status:redis - -# Clean up old Docker logs -docker system prune -``` - -### Backup Procedures - -#### PostgreSQL Backup -```bash -docker exec serpentrace-postgres-dev pg_dump -U postgres serpentrace > backup.sql -``` - -#### Redis Backup -```bash -docker exec serpentrace-redis-dev redis-cli BGSAVE -``` - -#### MinIO Backup -Use MinIO Console or mc client to backup buckets. - -## 🎯 Performance Optimization - -### PostgreSQL -- Monitor active connections with `npm run db:status:pg` -- Use connection pooling in production -- Regular VACUUM and ANALYZE operations - -### Redis -- Monitor memory usage -- Configure appropriate eviction policies -- Use Redis persistence (RDB/AOF) in production - -### MinIO -- Configure appropriate bucket policies -- Use lifecycle management for old files -- Monitor storage usage through console - -## 🚀 Quick Reference Commands - -```bash -# Status and Health -npm run db:status # Full system status -npm run test:connections # Quick connection test - -# Database Operations -npm run migration:run # Apply migrations -npm run migration:show # Check migration status - -# Docker Management -docker ps # Show running containers -docker logs # View logs -docker restart # Restart service - -# Direct Access -psql -h localhost -U postgres -d serpentrace # PostgreSQL CLI -redis-cli -h localhost # Redis CLI -``` - -## 🌐 Web Interfaces Summary - -| Service | URL | Credentials | -|---------|-----|------------| -| pgAdmin | http://localhost:8080 | admin@serpentrace.dev / admin | -| Redis Commander | http://localhost:8081 | No auth required | -| MinIO Console | http://localhost:9001 | serpentrace / serpentrace123 | -| Backend API | http://localhost:3000 | When running | -| Frontend | http://localhost:5173 | When running | - ---- - -*This guide is automatically updated when database configurations change. Last updated: 2025-08-23* diff --git a/Documentations/DOCKER_WATCHER_GUIDE.md b/Documentations/DOCKER_WATCHER_GUIDE.md deleted file mode 100644 index c7632d5a..00000000 --- a/Documentations/DOCKER_WATCHER_GUIDE.md +++ /dev/null @@ -1,235 +0,0 @@ -# Docker Watcher Implementation Guide - -## Overview - -This document explains the Docker watcher implementation for the SerpentRace project, which automatically synchronizes local file changes with Docker containers and rebuilds images when necessary. - -## What's Implemented - -### Docker Compose Watch Configuration - -The development Docker Compose configuration now includes `develop.watch` sections for both frontend and backend services that provide: - -1. **File Synchronization**: Automatically sync source code changes to running containers -2. **Selective Rebuilding**: Rebuild containers when critical configuration files change -3. **Intelligent Ignore Patterns**: Exclude unnecessary files like `node_modules` - -### Backend Watcher Configuration - -```yaml -develop: - watch: - - action: sync - path: ../SerpentRace_Backend/src - target: /app/src - ignore: - - node_modules/ - - action: sync - path: ../SerpentRace_Backend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Backend/package-lock.json - - action: rebuild - path: ../SerpentRace_Docker/Dockerfile_backend.dev -``` - -### Frontend Watcher Configuration - -```yaml -develop: - watch: - - action: sync - path: ../SerpentRace_Frontend/src - target: /app/src - ignore: - - node_modules/ - - action: sync - path: ../SerpentRace_Frontend/public - target: /app/public - - action: sync - path: ../SerpentRace_Frontend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Frontend/package-lock.json - - action: rebuild - path: ../SerpentRace_Frontend/vite.config.js - - action: rebuild - path: ../SerpentRace_Docker/Dockerfile_frontend.dev -``` - -## How It Works - -### Sync Actions -- **Purpose**: Instantly copy changed files from host to container -- **Use Cases**: Source code files, static assets, configuration files that don't require rebuild -- **Performance**: Near-instant updates, no container restart needed - -### Rebuild Actions -- **Purpose**: Trigger full container rebuild when critical files change -- **Use Cases**: Package files, Docker configuration, build configuration -- **Performance**: Takes longer but ensures consistency - -## Usage - -### New Commands Added - -#### Windows (docker-manage.bat) -```bash -# Start with file watchers -.\docker-manage.bat dev:watch - -# Traditional start (without watchers) -.\docker-manage.bat dev:start -``` - -#### Linux/Mac (docker-manage.sh) -```bash -# Start with file watchers -./docker-manage.sh dev:watch - -# Traditional start (without watchers) -./docker-manage.sh dev:start -``` - -### Command Differences - -| Command | Mode | File Watching | Container Rebuild | Use Case | -|---------|------|---------------|-------------------|----------| -| `dev:start` | Background (-d) | No | Manual only | Traditional development | -| `dev:watch` | Foreground | Yes | Automatic | Modern development with live sync | - -## Benefits - -### 1. Instant File Synchronization -- Source code changes are immediately available in containers -- No manual rebuild or restart required for code changes -- Maintains all existing hot-reload functionality (nodemon, Vite HMR) - -### 2. Smart Rebuilding -- Automatically rebuilds when package.json or Dockerfile changes -- Ensures containers stay consistent with dependency updates -- Prevents common issues with stale dependencies - -### 3. Development Efficiency -- Combines Docker's isolation with native-like development speed -- Reduces context switching between local and containerized development -- Maintains consistent environment across team members - -## File Patterns Watched - -### Backend -- **Synced Files**: - - `src/` directory (all TypeScript source files) - - `package.json` (for runtime reference) -- **Rebuild Triggers**: - - `package-lock.json` (dependency changes) - - `Dockerfile_backend.dev` (container configuration) - -### Frontend -- **Synced Files**: - - `src/` directory (React components, styles, etc.) - - `public/` directory (static assets) - - `package.json` (for runtime reference) -- **Rebuild Triggers**: - - `package-lock.json` (dependency changes) - - `vite.config.js` (build configuration) - - `Dockerfile_frontend.dev` (container configuration) - -## Performance Considerations - -### Sync Performance -- File synchronization is near-instantaneous -- Uses Docker's built-in file watching mechanisms -- Optimized for development workloads - -### Rebuild Performance -- Rebuilds only occur when necessary -- Docker layer caching reduces rebuild times -- Can be resource-intensive for large dependency changes - -## Troubleshooting - -### Common Issues - -1. **File Changes Not Reflected** - - Ensure you're using `dev:watch` command - - Check that files are not in ignore patterns - - Verify file paths are correct - -2. **Excessive Rebuilds** - - Check for unnecessary changes to rebuild trigger files - - Consider moving files to sync-only patterns if appropriate - -3. **Performance Issues** - - Monitor Docker resource usage - - Consider excluding large directories from watching - - Use `.dockerignore` for files that should never be synced - -### Debugging Commands - -```bash -# Check container status -docker-compose -f SerpentRace_Docker/docker-compose.dev.yml ps - -# View watcher logs -docker-compose -f SerpentRace_Docker/docker-compose.dev.yml logs -f backend -docker-compose -f SerpentRace_Docker/docker-compose.dev.yml logs -f frontend - -# Check file synchronization -docker exec -it serpentrace-backend-dev ls -la /app/src -docker exec -it serpentrace-frontend-dev ls -la /app/src -``` - -## Requirements - -### Docker Compose Version -- Requires Docker Compose v2.22+ for `develop.watch` support -- Check version: `docker-compose version` - -### File System -- Works on Windows, Linux, and macOS -- Performance may vary based on file system type -- WSL2 recommended for Windows users - -## Migration from Traditional Setup - -### No Breaking Changes -- Existing `dev:start` command continues to work -- All volume mounts remain functional -- Hot reload functionality preserved - -### Gradual Adoption -1. Try `dev:watch` for active development -2. Use `dev:start` for background services -3. Gradually migrate team to new workflow - -## Best Practices - -### Development Workflow -1. Use `dev:watch` during active development -2. Make code changes normally -3. Watch for automatic synchronization -4. Monitor logs for any sync issues - -### File Organization -- Keep frequently changed files in sync patterns -- Place build configuration in rebuild patterns -- Use `.dockerignore` for files that should never sync - -### Team Collaboration -- Document which command team members should use -- Ensure consistent Docker Compose version across team -- Share troubleshooting steps for common issues - -## Future Enhancements - -### Potential Improvements -1. **Selective Service Watching**: Watch only specific services -2. **Custom Ignore Patterns**: Per-developer ignore configurations -3. **Performance Monitoring**: Built-in sync performance metrics -4. **Integration with IDEs**: Better editor integration for sync status - -### Configuration Expansion -- Additional file patterns as needed -- Service-specific watch configurations -- Environment-based watch rules diff --git a/Documentations/FRONTEND_CODING_GUIDELINES.md b/Documentations/FRONTEND_CODING_GUIDELINES.md deleted file mode 100644 index 9902b368..00000000 --- a/Documentations/FRONTEND_CODING_GUIDELINES.md +++ /dev/null @@ -1,570 +0,0 @@ -# Frontend Kódolási Útmutató - SerpentRace - -## Tartalomjegyzék -1. [Navigáció és Routing](#navigáció-és-routing) -2. [Fájl és Mappa Struktúra](#fájl-és-mappa-struktúra) -3. [Komponens Konvenciók](#komponens-konvenciók) -4. [State Management](#state-management) -5. [API Hívások](#api-hívások) -6. [Hibakezelés](#hibakezelés) -7. [Elnevezési Konvenciók](#elnevezési-konvenciók) - ---- - -## Navigáció és Routing - -### ✅ Helyes gyakorlat: HandleNavigate használata - -**MINDIG használd a központosított HandleNavigate hook-ot navigációhoz:** - -```jsx -import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate" - -const MyComponent = () => { - const { goHome, goLogin, goDeckDetails } = HandleNavigate() - - const handleClick = () => { - goHome() // Egyszerű navigáció - } - - const handleDeckView = (deckId) => { - goDeckDetails(deckId) // Dinamikus route paraméterrel - } - - const handleLobby = (gameCode) => { - goLobby({ gameCode }) // State passzolással - } -} -``` - -### ❌ Kerülendő: Direkt useNavigate használat - -```jsx -// SOHA NE HASZNÁLD EZT! -import { useNavigate } from "react-router-dom" - -const MyComponent = () => { - const navigate = useNavigate() - navigate("/home") // ❌ NEM JÓ! -} -``` - -### Elérhető Navigációs Függvények - -**HandleNavigate által biztosított függvények:** - -```jsx -const { - // Általános - goTo, // goTo('/any-path', { state: {...} }) - goBack, // Vissza az előző oldalra - - // Authentikáció - goHome, // → /home - goLogin, // → /login, state: { success, message } - goRegister, // → /register (alias: goAuth) - goLanding, // → / (landing page) - - // Deck Management - goDecks, // → /decks - goDeckDetails, // goDeckDetails(deckId) → /deck/:deckId - goDeckCreator, // → /deck-creator - goDeckCreatorEdit, // goDeckCreatorEdit(deckId) → /deck-creator/:deckId - - // Game Flow - goLobby, // goLobby({ gameCode }) → /lobby - goChooseDeck, // goChooseDeck({ username, deckIds }) → /choosedeck - goPlayerSetup, // goPlayerSetup({ deckIds }) → /player-setup - goGame, // goGame({ players, gameState }) → /game - - // Egyéb - goContacts // → /contacts (alias: goCompanies) -} = HandleNavigate() -``` - -### Route Konstansok - -**Használd a centralizált route konstansokat:** - -```jsx -// src/utils/routes.js -import { ROUTES } from '../../utils/routes' - -// App.jsx-ben -} /> -} /> - -// ❌ NE használj string literálokat: -} /> // NEM JÓ! -``` - -### State Passzolás - -**Így adj át adatokat navigáció során:** - -```jsx -// Régi mód (useNavigate) - ❌ NE! -navigate('/lobby', { state: { gameCode: 'ABC123' } }) - -// Új mód (HandleNavigate) - ✅ JÓ! -goLobby({ gameCode: 'ABC123' }) - -// Fogadó oldalon: -import { useLocation } from 'react-router-dom' - -const Lobby = () => { - const location = useLocation() - const gameCode = location.state?.gameCode -} -``` - ---- - -## Fájl és Mappa Struktúra - -### Mappa Szervezés - -``` -src/ -├── api/ # API hívások -│ ├── userApi.js -│ ├── deckApi.js -│ └── gameApi.js -├── assets/ # Statikus fájlok -│ ├── backgrounds/ -│ ├── images/ -│ └── icons/ -├── components/ # Újrahasználható komponensek -│ ├── Buttons/ -│ ├── Inputs/ -│ ├── Navbar/ -│ └── PopUp/ -├── hooks/ # Custom Hooks -│ └── useRequireAuth.jsx -├── pages/ # Oldal komponensek -│ ├── Auth/ -│ ├── Game/ -│ ├── Decks/ -│ └── Landing/ -├── utils/ # Utility függvények -│ ├── HandleNavigate/ -│ └── routes.js -└── App.jsx # Fő alkalmazás komponens -``` - -### Fájl Elnevezési Konvenciók - -- **Komponensek**: PascalCase - - `LoginForm.jsx`, `DeckCreator.jsx`, `ButtonGreen.jsx` - -- **Utility fájlok**: camelCase - - `routes.js`, `randomUtils.js`, `userApi.js` - -- **Hook fájlok**: camelCase, "use" prefix - - `useRequireAuth.jsx`, `useLocalStorage.jsx` - ---- - -## Komponens Konvenciók - -### Funkcionális Komponens Sablon - -```jsx -import React, { useState, useEffect } from 'react' -import HandleNavigate from '../../utils/HandleNavigate/HandleNavigate' - -/** - * Komponens rövid leírása - * @returns {JSX.Element} - */ -const MyComponent = () => { - // 1. Hooks (HandleNavigate, useState, useEffect, stb.) - const { goHome } = HandleNavigate() - const [data, setData] = useState(null) - - // 2. Effect hooks - useEffect(() => { - // Component mount logic - }, []) - - // 3. Event handlers - const handleClick = () => { - // Logic - } - - // 4. Render - return ( -
- {/* JSX */} -
- ) -} - -export default MyComponent -``` - -### Import Sorrend - -```jsx -// 1. React és third-party library-k -import React, { useState } from 'react' -import { motion } from 'framer-motion' - -// 2. React Router hooks (useLocation, useParams - NEM useNavigate!) -import { useLocation } from 'react-router-dom' - -// 3. Custom hooks és utils -import HandleNavigate from '../../utils/HandleNavigate/HandleNavigate' -import useRequireAuth from '../../hooks/useRequireAuth' - -// 4. API -import { getUserData } from '../../api/userApi' - -// 5. Komponensek -import Button from '../../components/Buttons/Button' -import Navbar from '../../components/Navbar/Navbar' - -// 6. Assets -import Background from '../../assets/backgrounds/Background' -``` - ---- - -## State Management - -### Local State - -```jsx -// Egyszerű state -const [count, setCount] = useState(0) - -// Object state -const [user, setUser] = useState({ - name: '', - email: '' -}) - -// Array state -const [items, setItems] = useState([]) -``` - -### LocalStorage Használat - -**useRequireAuth hook használata auth kezeléshez:** - -```jsx -import useRequireAuth from '../../hooks/useRequireAuth' - -const MyComponent = () => { - const [username] = useRequireAuth({ - key: 'username', - redirectTo: '/login' - }) - - // username automatikusan szinkronizálva van localStorage-el - // Ha nincs username, automatikus redirect /login-re -} -``` - -**Manuális localStorage:** - -```jsx -// Írás -localStorage.setItem('gameToken', token) - -// Olvasás -const token = localStorage.getItem('gameToken') - -// Törlés -localStorage.removeItem('gameToken') -``` - ---- - -## API Hívások - -### API File Struktúra - -**Minden API endpoint egy külön file-ban (`userApi.js`, `deckApi.js`, `gameApi.js`):** - -```jsx -// src/api/userApi.js -import axiosInstance from './axiosInstance' - -export const getUserData = async (userId) => { - try { - const response = await axiosInstance.get(`/users/${userId}`) - return response.data - } catch (error) { - console.error('Error fetching user data:', error) - throw error - } -} - -export const updateUser = async (userId, userData) => { - try { - const response = await axiosInstance.put(`/users/${userId}`, userData) - return response.data - } catch (error) { - console.error('Error updating user:', error) - throw error - } -} -``` - -### API Hívás Komponensben - -```jsx -import { getUserData } from '../../api/userApi' - -const MyComponent = () => { - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [data, setData] = useState(null) - - const fetchData = async () => { - setLoading(true) - setError(null) - - try { - const result = await getUserData(userId) - setData(result) - } catch (err) { - setError(err.message || 'Hiba történt') - } finally { - setLoading(false) - } - } - - useEffect(() => { - fetchData() - }, [userId]) - - if (loading) return
Betöltés...
- if (error) return
Hiba: {error}
- - return
{/* data megjelenítése */}
-} -``` - ---- - -## Hibakezelés - -### Try-Catch Blokkok - -```jsx -const handleSubmit = async () => { - try { - const response = await createDeck(deckData) - // Siker kezelése - notifySuccess('Deck sikeresen létrehozva!') - goDecks() - } catch (error) { - // Hiba kezelése - const errorMessage = error.response?.data?.message || 'Ismeretlen hiba' - setError(errorMessage) - notifyError(errorMessage) - } -} -``` - -### Toast Notifications - -```jsx -import { notifySuccess, notifyError } from '../../components/Toastify/toastifyServices' - -// Siker üzenet -notifySuccess('✅ Művelet sikeres!') - -// Hiba üzenet -notifyError('❌ Hiba történt!') - -// Egyedi konfiguráció -notifySuccess('Mentve!', { autoClose: 2000 }) -``` - ---- - -## Elnevezési Konvenciók - -### JavaScript/React - -| Típus | Konvenció | Példa | -|-------|-----------|-------| -| Komponensek | PascalCase | `LoginForm`, `DeckCreator` | -| Függvények | camelCase | `handleClick`, `fetchUserData` | -| Változók | camelCase | `userName`, `isLoading` | -| Konstansok | UPPER_SNAKE_CASE | `API_BASE_URL`, `MAX_PLAYERS` | -| Private változók | _camelCase | `_internalState` | -| Event handlers | handle + PascalCase | `handleSubmit`, `handleInputChange` | -| Boolean változók | is/has/can prefix | `isLoading`, `hasError`, `canEdit` | - -### CSS Classes (Tailwind) - -```jsx -// Használj explicit class neveket -
- -// Kerüld a túl hosszú class stringeket - bontsd több sorra -
-``` - -### Fájl Nevek - -- **Egyedi komponens**: `LoginForm.jsx` (nem `login-form.jsx`) -- **Index fájlok**: `index.jsx` (ha könyvtárban több file van) -- **Utility fájlok**: `randomUtils.js` (camelCase) -- **API fájlok**: `userApi.js` (camelCase + Api postfix) - ---- - -## Teljes Példa - Best Practices - -```jsx -// src/pages/Example/ExamplePage.jsx - -import React, { useState, useEffect } from 'react' -import { useLocation } from 'react-router-dom' -import { motion } from 'framer-motion' - -import HandleNavigate from '../../utils/HandleNavigate/HandleNavigate' -import useRequireAuth from '../../hooks/useRequireAuth' -import { fetchExampleData, updateExampleData } from '../../api/exampleApi' -import { notifySuccess, notifyError } from '../../components/Toastify/toastifyServices' - -import Navbar from '../../components/Navbar/Navbar' -import Button from '../../components/Buttons/Button' -import Background from '../../assets/backgrounds/Background' - -/** - * Example Page - Komponens rövid leírása - * @returns {JSX.Element} - */ -const ExamplePage = () => { - // 1. Auth & Navigation - const [username] = useRequireAuth({ key: 'username', redirectTo: '/login' }) - const { goHome, goBack } = HandleNavigate() - const location = useLocation() - - // 2. State - const [data, setData] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - // 3. Effects - useEffect(() => { - loadData() - }, []) - - // 4. Functions - const loadData = async () => { - setLoading(true) - setError(null) - - try { - const result = await fetchExampleData() - setData(result) - } catch (err) { - const errorMsg = err.response?.data?.message || 'Hiba történt' - setError(errorMsg) - notifyError(errorMsg) - } finally { - setLoading(false) - } - } - - const handleSave = async () => { - try { - await updateExampleData(data) - notifySuccess('✅ Sikeresen mentve!') - goHome() - } catch (err) { - notifyError('❌ Mentés sikertelen') - } - } - - const handleCancel = () => { - goBack() - } - - // 5. Conditional Rendering - if (loading) { - return ( -
-
Betöltés...
-
- ) - } - - if (error) { - return ( -
-
Hiba: {error}
-
- ) - } - - // 6. Main Render - return ( -
- - - -
- -

Example Page

- - {/* Content */} -
- {data && ( -
- {/* Render data */} -
- )} -
- - {/* Actions */} -
-
-
-
-
- ) -} - -export default ExamplePage -``` - ---- - -## Összefoglalás - Legfontosabb Szabályok - -1. ✅ **MINDIG használd HandleNavigate-et** navigációhoz -2. ✅ **Használd a ROUTES konstansokat** az App.jsx-ben -3. ✅ **API hívások külön file-okban** (userApi.js, deckApi.js, stb.) -4. ✅ **Try-catch minden async műveletnél** -5. ✅ **Toast notifications** a felhasználói visszajelzéshez -6. ✅ **useRequireAuth hook** auth védett oldalaknál -7. ✅ **Konzisztens import sorrend** -8. ✅ **PascalCase komponenseknek, camelCase változóknak** -9. ❌ **SOHA ne használj useNavigate közvetlen** -10. ❌ **Ne használj string literal route-okat** - ---- - -**Verzió:** 1.0 -**Utolsó frissítés:** 2025-01-17 -**Készítette:** SerpentRace Development Team diff --git a/Documentations/FRONTEND_GAME_COMPLETION.md b/Documentations/FRONTEND_GAME_COMPLETION.md deleted file mode 100644 index bce9ed09..00000000 --- a/Documentations/FRONTEND_GAME_COMPLETION.md +++ /dev/null @@ -1,476 +0,0 @@ -# Frontend Game Completion Implementation - -**Date:** November 19, 2025 -**Status:** ✅ Core gameplay event handlers and action methods implemented - ---- - -## Overview - -This document details the completion of missing WebSocket event handlers and action methods in the frontend to enable full gameplay functionality. The implementation ensures that GameScreen can properly receive and respond to all game events from the backend. - ---- - -## Changes Implemented - -### 1. GameWebSocketContext.jsx - Added Missing Event Handlers - -**Location:** `SerpentRace_Frontend/src/contexts/GameWebSocketContext.jsx` - -Added 9 critical gameplay event handlers that were missing from the context: - -#### ✅ game:your-turn -- **Purpose:** Notifies player when it's their turn -- **Action:** Updates currentTurn state, emits custom event for GameScreen -- **Implementation:** -```javascript -socket.on('game:your-turn', (data) => { - log('🎯 Your turn!', data); - setCurrentTurn(data.currentPlayer); - window.dispatchEvent(new CustomEvent('game:your-turn', { detail: data })); -}); -``` - -#### ✅ game:dice-rolled -- **Purpose:** Broadcasts dice roll results to all players -- **Action:** Emits custom event for UI to display dice animation -- **Implementation:** -```javascript -socket.on('game:dice-rolled', (data) => { - log('🎲 Dice rolled:', data.diceValue, 'by', data.playerName); - window.dispatchEvent(new CustomEvent('game:dice-rolled', { detail: data })); -}); -``` - -#### ✅ game:guess-result -- **Purpose:** Receives position guess validation result -- **Action:** Updates player position if guess was correct, emits event -- **Implementation:** -```javascript -socket.on('game:guess-result', (data) => { - log('🎯 Guess result:', data); - if (data.correct && data.newPosition !== undefined) { - setBoardData(prev => { - if (!prev) return prev; - const updatedPlayers = { ...prev.playerPositions }; - updatedPlayers[data.playerName] = data.newPosition; - return { ...prev, playerPositions: updatedPlayers }; - }); - } - window.dispatchEvent(new CustomEvent('game:guess-result', { detail: data })); -}); -``` - -#### ✅ game:joker-complete -- **Purpose:** Receives joker card approval/rejection result -- **Action:** Updates player position if joker was approved, emits event -- **Implementation:** -```javascript -socket.on('game:joker-complete', (data) => { - log('🃏 Joker complete:', data); - if (data.approved && data.newPosition !== undefined) { - setBoardData(prev => { - if (!prev) return prev; - const updatedPlayers = { ...prev.playerPositions }; - updatedPlayers[data.playerName] = data.newPosition; - return { ...prev, playerPositions: updatedPlayers }; - }); - } - window.dispatchEvent(new CustomEvent('game:joker-complete', { detail: data })); -}); -``` - -#### ✅ game:luck-consequence -- **Purpose:** Receives luck card consequence (extra turns, lost turns, position changes) -- **Action:** Updates player position if consequence includes movement, emits event -- **Implementation:** -```javascript -socket.on('game:luck-consequence', (data) => { - log('🍀 Luck consequence:', data); - if (data.newPosition !== undefined && data.playerName) { - setBoardData(prev => { - if (!prev) return prev; - const updatedPlayers = { ...prev.playerPositions }; - updatedPlayers[data.playerName] = data.newPosition; - return { ...prev, playerPositions: updatedPlayers }; - }); - } - window.dispatchEvent(new CustomEvent('game:luck-consequence', { detail: data })); -}); -``` - -#### ✅ game:ended -- **Purpose:** Announces game end with winner and final scores -- **Action:** Updates gameState with winner and final scores, emits event for winner modal -- **Implementation:** -```javascript -socket.on('game:ended', (data) => { - log('🏁 Game ended! Winner:', data.winner); - setGameState(prev => ({ - ...prev, - status: 'finished', - winner: data.winner, - finalScores: data.scores - })); - window.dispatchEvent(new CustomEvent('game:ended', { detail: data })); -}); -``` - -#### ✅ game:extra-turn-remaining -- **Purpose:** Notifies player they have extra turn(s) from luck consequences -- **Action:** Emits event for UI notification -- **Implementation:** -```javascript -socket.on('game:extra-turn-remaining', (data) => { - log('⭐ Extra turn remaining:', data); - window.dispatchEvent(new CustomEvent('game:extra-turn-remaining', { detail: data })); -}); -``` - -#### ✅ game:players-skipped -- **Purpose:** Broadcasts when players are skipped due to lost turn consequences -- **Action:** Emits event for UI notification -- **Implementation:** -```javascript -socket.on('game:players-skipped', (data) => { - log('⏭️ Players skipped:', data.skippedPlayers); - window.dispatchEvent(new CustomEvent('game:players-skipped', { detail: data })); -}); -``` - -#### ✅ game:cleanup-complete -- **Purpose:** Confirms cleanup after game end -- **Action:** Emits event for final UI state reset -- **Implementation:** -```javascript -socket.on('game:cleanup-complete', (data) => { - log('🧹 Cleanup complete:', data); - window.dispatchEvent(new CustomEvent('game:cleanup-complete', { detail: data })); -}); -``` - ---- - -### 2. GameWebSocketContext.jsx - Added Missing Action Methods - -Added 4 critical action methods that players and gamemaster need: - -#### ✅ submitAnswer(answer) -- **Purpose:** Submit answer to question card -- **Parameters:** `answer` - Player's answer (type depends on card type: string for QUIZ/OWN_ANSWER, array for SENTENCE_PAIRING, boolean for TRUE_FALSE, number for CLOSER) -- **Emits:** `game:card-answer` with gameCode and answer -- **Returns:** Boolean (success/failure) - -```javascript -const submitAnswer = useCallback((answer) => { - const socket = socketRef.current; - if (!socket || !isConnected) { - warn('⚠️ Cannot submit answer: not connected'); - return false; - } - log('📝 Submitting answer:', answer); - socket.emit('game:card-answer', { gameCode: gameState?.gameCode, answer }); - return true; -}, [isConnected, gameState?.gameCode]); -``` - -#### ✅ submitPositionGuess(guessedPosition) -- **Purpose:** Submit position guess after correct answer -- **Parameters:** `guessedPosition` - Number (0-99) representing guessed board position -- **Emits:** `game:position-guess` with gameCode and guessedPosition -- **Returns:** Boolean (success/failure) - -```javascript -const submitPositionGuess = useCallback((guessedPosition) => { - const socket = socketRef.current; - if (!socket || !isConnected) { - warn('⚠️ Cannot submit position guess: not connected'); - return false; - } - log('🎯 Submitting position guess:', guessedPosition); - socket.emit('game:position-guess', { gameCode: gameState?.gameCode, guessedPosition }); - return true; -}, [isConnected, gameState?.gameCode]); -``` - -#### ✅ approveJoker(requestId) -- **Purpose:** Gamemaster approves joker card -- **Parameters:** `requestId` - Unique identifier for joker decision request -- **Emits:** `game:gamemaster-decision` with gameCode, requestId, decision: 'approve' -- **Returns:** Boolean (success/failure) -- **Authorization:** Requires isGamemaster = true - -```javascript -const approveJoker = useCallback((requestId) => { - const socket = socketRef.current; - if (!socket || !isConnected || !isGamemaster) { - warn('⚠️ Cannot approve joker: not gamemaster or not connected'); - return false; - } - log('✅ Approving joker request:', requestId); - socket.emit('game:gamemaster-decision', { - gameCode: gameState?.gameCode, - requestId, - decision: 'approve' - }); - return true; -}, [isConnected, isGamemaster, gameState?.gameCode]); -``` - -#### ✅ rejectJoker(requestId, reason?) -- **Purpose:** Gamemaster rejects joker card -- **Parameters:** - - `requestId` - Unique identifier for joker decision request - - `reason` - Optional rejection reason (default: 'Joker answer rejected') -- **Emits:** `game:gamemaster-decision` with gameCode, requestId, decision: 'reject', reason -- **Returns:** Boolean (success/failure) -- **Authorization:** Requires isGamemaster = true - -```javascript -const rejectJoker = useCallback((requestId, reason = 'Joker answer rejected') => { - const socket = socketRef.current; - if (!socket || !isConnected || !isGamemaster) { - warn('⚠️ Cannot reject joker: not gamemaster or not connected'); - return false; - } - log('❌ Rejecting joker request:', requestId, 'Reason:', reason); - socket.emit('game:gamemaster-decision', { - gameCode: gameState?.gameCode, - requestId, - decision: 'reject', - reason - }); - return true; -}, [isConnected, isGamemaster, gameState?.gameCode]); -``` - ---- - -### 3. GameWebSocketContext.jsx - Updated Context Value Export - -Updated the context value to export all new methods: - -```javascript -const value = { - socket: socketRef.current, - isConnected, - gameState, - players, - boardData, - currentTurn, - error, - isGamemaster, - gameStarted, - pendingPlayers, - approvalStatus, - // Connection management - connect, - disconnect, - // Methods - rollDice, - sendMessage, - setReady, - leaveGame, - approvePlayer, - rejectPlayer, - submitAnswer, // ✅ NEW - submitPositionGuess, // ✅ NEW - approveJoker, // ✅ NEW - rejectJoker, // ✅ NEW - addEventListener, - removeEventListener, -}; -``` - ---- - -### 4. GameScreen.jsx - Fixed Action Method Calls - -**Location:** `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx` - -#### Fixed handleSubmitAnswer -**Before:** -```javascript -const handleSubmitAnswer = useCallback((answer) => { - if (currentCard?.id) { - submitAnswer(currentCard.id, answer) // ❌ Wrong parameters - } -}, [currentCard?.id, submitAnswer]) -``` - -**After:** -```javascript -const handleSubmitAnswer = useCallback((answer) => { - console.log('📝 Válasz beküldve:', answer) - submitAnswer(answer) // ✅ Correct - backend extracts gameCode from context -}, [submitAnswer]) -``` - -#### Fixed handleApproveJoker -**Before:** -```javascript -const handleApproveJoker = useCallback(async (jokerRequest) => { - approveJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId) // ❌ Wrong parameters - setIsJokerModalOpen(false) -}, [approveJoker]) -``` - -**After:** -```javascript -const handleApproveJoker = useCallback(async (jokerRequest) => { - console.log('✅ Joker feladat jóváhagyva:', jokerRequest) - approveJoker(jokerRequest.requestId) // ✅ Correct - only requestId needed - setIsJokerModalOpen(false) -}, [approveJoker]) -``` - -#### Fixed handleRejectJoker -**Before:** -```javascript -const handleRejectJoker = useCallback(async (jokerRequest) => { - rejectJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId) // ❌ Wrong parameters - setIsJokerModalOpen(false) -}, [rejectJoker]) -``` - -**After:** -```javascript -const handleRejectJoker = useCallback(async (jokerRequest) => { - console.log('❌ Joker feladat elutasítva:', jokerRequest) - rejectJoker(jokerRequest.requestId, 'Joker rejected by gamemaster') // ✅ Correct - setIsJokerModalOpen(false) -}, [rejectJoker]) -``` - ---- - -## Architecture Benefits - -### ✅ Centralized Event Handling -All WebSocket events are handled in the context, ensuring: -- Single source of truth for game state -- Consistent state updates across all components -- Easy debugging with centralized logging - -### ✅ Custom Event Bridge -Events are re-emitted as CustomEvents via `window.dispatchEvent()`, allowing: -- GameScreen to add specific UI logic without modifying context -- Separation of concerns (state management vs UI presentation) -- Multiple components can listen to the same events independently - -### ✅ Persistent Connection -Socket connection persists across navigation (Lobby → GameScreen), ensuring: -- No disconnections during page transitions -- Gamemaster can start game without socket dropping -- Real-time updates continue seamlessly - -### ✅ Type Safety & Validation -All action methods include: -- Connection state checks (`isConnected`) -- Authorization checks (`isGamemaster` for approval methods) -- Error logging for debugging -- Boolean return values for success/failure - ---- - -## Testing Checklist - -### ✅ Event Handler Tests -- [ ] Test `game:your-turn` - Turn indicator updates -- [ ] Test `game:dice-rolled` - Dice animation triggers -- [ ] Test `game:guess-result` - Position updates on correct guess -- [ ] Test `game:joker-complete` - Position updates on approved joker -- [ ] Test `game:luck-consequence` - Position updates from luck cards -- [ ] Test `game:ended` - Winner modal displays with final scores -- [ ] Test `game:extra-turn-remaining` - Extra turn notification -- [ ] Test `game:players-skipped` - Skip notification -- [ ] Test `game:cleanup-complete` - Cleanup confirmation - -### ✅ Action Method Tests -- [ ] Test `submitAnswer()` - Answer submission for all card types (QUIZ, SENTENCE_PAIRING, TRUE_FALSE, CLOSER, OWN_ANSWER) -- [ ] Test `submitPositionGuess()` - Position guess submission -- [ ] Test `approveJoker()` - Gamemaster approval (requires isGamemaster) -- [ ] Test `rejectJoker()` - Gamemaster rejection (requires isGamemaster) - -### ✅ Integration Tests -- [ ] Complete game flow: Start → Dice → Card → Answer → Position Guess → Next Turn -- [ ] Joker flow: Joker drawn → Request sent → Gamemaster decision → Position update -- [ ] Luck flow: Luck card → Consequence applied → Position/turn updated -- [ ] End game flow: Player reaches finish → Winner announced → Scores displayed - ---- - -## Remaining UI Enhancements - -### 🎨 Turn Indicator Component -**Status:** Not implemented -**Description:** Visual indicator showing whose turn it is -**Events:** `game:your-turn`, `game:turn-changed` -**Location:** GameScreen.jsx header area - -### ⏱️ Timer Component -**Status:** Not implemented -**Description:** Countdown timer for card answers (60s) and joker decisions (120s) -**Events:** `game:card-drawn-self`, `game:gamemaster-decision-request` -**Location:** CardDisplayModal, JokerApprovalModal - -### 🏆 Winner Modal -**Status:** Not implemented -**Description:** Full-screen modal showing winner, final scores, and play again option -**Events:** `game:ended` -**Location:** GameScreen.jsx (new modal component) - -### ✨ Position Update Animations -**Status:** Not implemented -**Description:** Smooth token movement animations for position changes -**Events:** `game:player-moved`, `game:guess-result`, `game:joker-complete`, `game:luck-consequence` -**Location:** GameScreen.jsx player token rendering - -### 📊 Score Display -**Status:** Not implemented -**Description:** Live leaderboard showing player rankings -**State:** `players` array with position data -**Location:** GameScreen.jsx sidebar or header - ---- - -## Known Issues & Future Work - -### 🐛 Known Issues -None currently - all core functionality implemented and error-free. - -### 🚀 Future Enhancements -1. **Notification System** - Toast/notification UI for game events -2. **Sound Effects** - Audio feedback for dice, cards, turns -3. **Animation Polish** - Smooth transitions for all state changes -4. **Mobile Responsiveness** - Touch-friendly controls for mobile devices -5. **Accessibility** - ARIA labels, keyboard navigation, screen reader support -6. **Reconnection Logic** - Handle network interruptions gracefully -7. **Spectator Mode** - Allow non-playing users to watch games -8. **Chat System** - Player communication during game - ---- - -## Summary - -✅ **9 critical event handlers** added to GameWebSocketContext -✅ **4 essential action methods** added to GameWebSocketContext -✅ **3 handler fixes** in GameScreen for correct parameter usage -✅ **Zero compilation errors** - all changes validated -✅ **Full gameplay flow** now supported by frontend - -The frontend is now **functionally complete** for core gameplay. Players can: -- Receive turn notifications -- Roll dice and move -- Draw and answer cards -- Submit position guesses -- Complete joker challenges (with gamemaster approval) -- Experience luck consequences -- See game end with winner announcement - -Remaining work is **UI polish** (animations, timers, winner screen) rather than functional gaps. - ---- - -**Last Updated:** November 19, 2025 -**Next Steps:** Implement UI enhancements and run comprehensive integration tests. diff --git a/Documentations/FRONTEND_IMPLEMENTATION_GUIDE_COMPLETE.md b/Documentations/FRONTEND_IMPLEMENTATION_GUIDE_COMPLETE.md deleted file mode 100644 index 22c5cde4..00000000 --- a/Documentations/FRONTEND_IMPLEMENTATION_GUIDE_COMPLETE.md +++ /dev/null @@ -1,1439 +0,0 @@ -# SerpentRace Backend API Documentation for Frontend Developers -## Complete API Reference with All Endpoints - -## Table of Contents -1. [Test User Credentials](#test-user-credentials) -2. [Data Structures & Entities](#data-structures--entities) -3. [Base URL & Service Info](#base-url--service-info) -4. [Authentication Endpoints](#authentication-endpoints) -5. [User Management](#user-management) -6. [Deck Management](#deck-management) -7. [Organization Management](#organization-management) -8. [Chat System](#chat-system) -9. [Contact Management](#contact-management) -10. [Import/Export Functionality](#importexport-functionality) -11. [Admin Endpoints](#admin-endpoints) -12. [Error Handling](#error-handling) -13. [WebSocket Real-Time Communication](#websocket-real-time-communication) - ---- - -## Test User Credentials - -For development and testing, use these pre-configured user accounts: - -### Regular User (Verified) -- **Username:** `john_doe` -- **Password:** `password123` -- **Email:** `john.doe@email.com` -- **Type:** Regular user (state: 1 - VERIFIED_REGULAR) -- **Organization:** None - -### Premium User (Organization Member) -- **Username:** `jane_premium` -- **Password:** `password123` -- **Email:** `jane.smith@email.com` -- **Type:** Premium user (state: 2 - VERIFIED_PREMIUM) -- **Organization:** Tech Solutions Inc - -### Teacher (Premium Organization Member) -- **Username:** `teacher_bob` -- **Password:** `password123` -- **Email:** `bob.teacher@eduinst.edu` -- **Type:** Premium user (state: 2 - VERIFIED_PREMIUM) -- **Organization:** Educational Institute - -### Admin User -- **Username:** `admin_user` -- **Password:** `password123` -- **Email:** `admin@serpentrace.com` -- **Type:** Admin (state: 5 - ADMIN) -- **Organization:** None - -### Unverified User -- **Username:** `new_user` -- **Password:** `password123` -- **Email:** `newuser@email.com` -- **Type:** Unverified (state: 0 - REGISTERED_NOT_VERIFIED) -- **Organization:** None - ---- - -## Data Structures & Entities - -### User DTOs -```typescript -interface ShortUserDto { - id: string; // UUID - username: string; // Username - state: number; // UserState enum - authLevel: 0 | 1; // 0 = regular, 1 = admin -} - -interface DetailUserDto { - id: string; // UUID - orgid: string | null; // Organization ID (if member) - username: string; // Unique username - email: string; // Email address - fname: string; // First name - lname: string; // Last name - code: string | null; // Verification code - type: string; // 'personal' | 'premium' | 'admin' - phone: string | null; // Phone number - state: number; // UserState enum value -} - -enum UserState { - REGISTERED_NOT_VERIFIED = 0, // Email not verified - VERIFIED_REGULAR = 1, // Regular verified user - VERIFIED_PREMIUM = 2, // Premium verified user - SOFT_DELETE = 3, // Soft deleted - DEACTIVATED = 4, // Account deactivated - ADMIN = 5 // Admin user -} -``` - -### Deck DTOs -```typescript -interface ShortDeckDto { - id: string; // UUID - name: string; // Deck name - type: number; // DeckType enum value - playedNumber: number; // Times played - ctype: number; // DeckVisibility enum value -} - -interface DetailDeckDto { - id: string; // UUID - name: string; // Deck name - type: number; // DeckType enum value - userid: string; // Owner's user ID - creationdate: Date; // Creation timestamp - cards: Card[]; // Array of cards - playedNumber: number; // Times played - ctype: number; // DeckVisibility enum value -} - -interface Card { - text: string; // Question/prompt text - type?: number; // CardType enum (optional) - answer?: string | null; // Answer (varies by type) -} - -enum DeckType { - LUCK = 0, // Luck-based cards - JOKER = 1, // Joker/wild cards - QUESTION = 2 // Question-based cards -} - -enum DeckVisibility { - PUBLIC = 0, // Public to all - PRIVATE = 1, // Private to owner - ORGANIZATION = 2 // Shared within organization -} - -enum DeckState { - ACTIVE = 0, // Active deck - SOFT_DELETE = 1 // Soft deleted -} - -enum CardType { - QUIZ = 0, // Multiple choice question - SENTENCE_PAIRING = 1, // Sentence completion - OWN_ANSWER = 2, // Custom answer - TRUE_FALSE = 3, // True/False question - CLOSER = 4 // Closer to answer -} -``` - -### Organization DTOs -```typescript -interface ShortOrganizationDto { - id: string; // UUID - name: string; // Organization name - contactfname: string; // Contact first name - contactlname: string; // Contact last name - contactemail: string; // Contact email - state: number; // OrganizationState enum - regdate: Date; // Registration date - maxOrganizationalDecks: number | null; // Max org decks allowed -} - -enum OrganizationState { - REGISTERED = 0, // Just registered - ACTIVE = 1, // Active organization - SOFT_DELETE = 2 // Soft deleted -} -``` - -### Chat DTOs -```typescript -interface ShortChatDto { - id: string; // UUID - userCount: number; // Number of participants - state: number; // ChatState enum value -} - -interface DetailChatDto { - id: string; // UUID - users: string[]; // Participant user IDs - messages: Message[]; // Message history - updateDate: Date; // Last update - state: number; // ChatState enum value -} - -interface Message { - id: string; // Message UUID - date: Date; // Message timestamp - userid: string; // Sender user ID - text: string; // Message content -} - -enum ChatType { - DIRECT = 'direct', // Direct message - GROUP = 'group', // Group chat - GAME = 'game' // Game-specific chat -} - -enum ChatState { - ACTIVE = 0, // Active chat - ARCHIVE = 1, // Archived chat - SOFT_DELETE = 2 // Soft deleted -} -``` - -### Contact DTOs -```typescript -interface ContactDto { - id: string; // UUID - name: string; // Contact name - email: string; // Contact email - userid: string | null; // User ID if logged in - type: number; // ContactType enum - txt: string; // Message content - state: number; // ContactState enum - createDate: Date; // Creation date - updateDate: Date; // Last update - adminResponse: string | null; // Admin response - responseDate: Date | null; // Response date - respondedBy: string | null; // Responding admin ID -} - -enum ContactType { - BUG = 0, // Bug report - PROBLEM = 1, // Problem report - QUESTION = 2, // General question - SALES = 3, // Sales inquiry - OTHER = 4 // Other type -} - -enum ContactState { - ACTIVE = 0, // Active/unresolved - RESOLVED = 1, // Resolved - SOFT_DELETE = 2 // Soft deleted -} -``` - ---- - -## Base URL & Service Info - -**Base URL:** `http://localhost:3000` (development) - -### Service Information -**Endpoint:** `GET /` - -**Authentication:** None required - -**Response Data:** -```typescript -{ - service: "SerpentRace Backend API"; - status: "running"; - version: "1.0.0"; - endpoints: { - swagger: "/api-docs"; - users: "/api/users"; - organizations: "/api/organizations"; - decks: "/api/decks"; - chats: "/api/chats"; - contacts: "/api/contacts"; - admin: "/api/admin"; - deckImportExport: "/api/deck-import-export"; - health: "/health"; - }; - websocket: { - enabled: true; - events: string[]; // WebSocket event names - }; -} -``` - -### Health Check -**Endpoint:** `GET /health` - -**Authentication:** None required - -**Response Data:** -```typescript -{ - status: "healthy" | "unhealthy"; - timestamp: string; // ISO timestamp - service: "SerpentRace Backend API"; - version: "1.0.0"; - environment: string; // "development" | "production" - database: { - connected: boolean; // Database connection status - type: string; // Database type - }; - websocket: { - enabled: boolean; // WebSocket status - }; - uptime: number; // Process uptime in seconds -} -``` - ---- - -## Authentication Endpoints - -### User Login -**Endpoint:** `POST /api/users/login` - -**Authentication:** None required - -**Validation Rules:** -- `username`: 3-50 characters -- `password`: 6-100 characters - -**Request Data:** -```typescript -{ - username: string; // Username or email - password: string; // Password -} -``` - -**Response Data (Success):** -```typescript -{ - token: string; // JWT token (also set as cookie) - user: ShortUserDto; // User information - organizationName?: string; // Organization name (if member) -} -``` - -**Error Responses:** -- `400`: Validation error -- `401`: Invalid credentials, unverified email, or account restrictions -- `500`: Internal server error - -### User Registration -**Endpoint:** `POST /api/users/create` - -**Authentication:** None required - -**Validation Rules:** -- `username`: 3-50 characters, unique -- `email`: valid email format, unique -- `password`: 6-100 characters - -**Request Data:** -```typescript -{ - username: string; // Unique username - email: string; // Valid email - password: string; // Password - fname?: string; // First name (optional) - lname?: string; // Last name (optional) - phone?: string; // Phone number (optional) - type?: string; // User type (optional) -} -``` - -**Response Data (Success):** -```typescript -{ - id: string; // User UUID - username: string; // Username - email: string; // Email - regdate: Date; // Registration date -} -``` - -**Error Responses:** -- `400`: Validation error -- `409`: Username or email already exists -- `500`: Internal server error - ---- - -## User Management - -### Get User Profile -**Endpoint:** `GET /api/users/profile` - -**Authentication:** Required - -**Response Data:** `DetailUserDto` - -### Update User Profile -**Endpoint:** `PATCH /api/users/profile` - -**Authentication:** Required - -**Request Data:** Partial `DetailUserDto` fields to update - -**Response Data:** Updated `DetailUserDto` - -**Error Responses:** -- `400`: Validation error -- `404`: User not found -- `409`: Email already exists -- `500`: Internal server error - ---- - -## Deck Management - -### Get Decks (Paginated) - RECOMMENDED -**Endpoint:** `GET /api/decks/page/{from}/{to}` - -**Authentication:** Required - -**URL Parameters:** -- `from`: Start index (0-based, ≥ 0) -- `to`: End index (inclusive, ≥ from) - -**Response Data:** -```typescript -{ - decks: ShortDeckDto[]; // Array of deck summaries - totalCount: number; // Total available decks -} -``` - -### Create Deck -**Endpoint:** `POST /api/decks` - -**Authentication:** Required - -**Request Data:** -```typescript -{ - name: string; // Deck name (required) - type?: number; // DeckType enum (optional) - cards?: Card[]; // Initial cards (optional) - ctype?: number; // DeckVisibility enum (optional) -} -``` - -**Response Data:** `ShortDeckDto` - -### Search Decks -**Endpoint:** `GET /api/decks/search` - -**Authentication:** Required - -**Query Parameters:** -- `query`: Search query (required, non-empty) -- `limit`: Results limit (1-100, default: 20) -- `offset`: Results offset (≥ 0, default: 0) - -**Response Data:** Array of matching `ShortDeckDto` - -### Get Deck by ID -**Endpoint:** `GET /api/decks/{id}` - -**Authentication:** Required - -**URL Parameters:** -- `id`: Deck UUID - -**Response Data:** `DetailDeckDto` - -### Update Deck -**Endpoint:** `PUT /api/decks/{id}` - -**Authentication:** Required (owner only) - -**URL Parameters:** -- `id`: Deck UUID - -**Request Data:** Partial `DetailDeckDto` fields to update - -**Response Data:** Updated `ShortDeckDto` - -### Delete Deck (Soft Delete) -**Endpoint:** `DELETE /api/decks/{id}` - -**Authentication:** Required (owner only) - -**URL Parameters:** -- `id`: Deck UUID - -**Response Data:** -```typescript -{ - success: boolean; // Deletion success status -} -``` - ---- - -## Organization Management - -### Get Organizations (Paginated) - RECOMMENDED -**Endpoint:** `GET /api/organizations/page/{from}/{to}` - -**Authentication:** Required - -**URL Parameters:** -- `from`: Start index (0-based, ≥ 0) -- `to`: End index (inclusive, ≥ from) - -**Response Data:** -```typescript -{ - organizations: ShortOrganizationDto[]; - totalCount: number; -} -``` - -### Search Organizations -**Endpoint:** `GET /api/organizations/search` - -**Authentication:** Required - -**Query Parameters:** -- `query`: Search query (required, non-empty) -- `limit`: Results limit (1-100, default: 20) -- `offset`: Results offset (≥ 0, default: 0) - -**Response Data:** Array of matching organizations - -### Get Organization Login URL -**Endpoint:** `GET /api/organizations/{orgId}/login-url` - -**Authentication:** Required - -**URL Parameters:** -- `orgId`: Organization UUID - -**Response Data:** -```typescript -{ - loginUrl: string; // Organization login URL - organizationName: string; // Organization name -} -``` - -### Process Organization Auth Callback -**Endpoint:** `POST /api/organizations/auth-callback` - -**Authentication:** Required - -**Request Data:** -```typescript -{ - organizationId: string; // Organization UUID - status: "ok" | "not_ok"; // Authentication status - authToken?: string; // Authentication token (optional) -} -``` - -**Response Data:** -```typescript -{ - success: boolean; // Processing success - message: string; // Result message - updatedFields?: string[]; // Fields that were updated -} -``` - ---- - -## Chat System - -### Get User Chats -**Endpoint:** `GET /api/chats/user-chats` - -**Authentication:** Required - -**Query Parameters:** -- `includeArchived`: boolean (default: false) - -**Response Data:** Array of `ShortChatDto` - -### Get Chat History -**Endpoint:** `GET /api/chats/history/{chatId}` - -**Authentication:** Required - -**URL Parameters:** -- `chatId`: Chat UUID (validated) - -**Response Data:** -```typescript -{ - id: string; // Chat UUID - users: string[]; // Participants - messages: Message[]; // Message history - isArchived: boolean; // Archive status - state: number; // Chat state -} -``` - -### Create Chat -**Endpoint:** `POST /api/chats/create` - -**Authentication:** Required - -**Validation Rules:** -- `type`: must be 'direct' or 'group' -- `userIds`: non-empty array -- `name`: required for groups - -**Request Data:** -```typescript -{ - type: 'direct' | 'group'; // Chat type - userIds: string[]; // Participant user IDs - name?: string; // Group name (required for groups) -} -``` - -**Response Data:** -```typescript -{ - id: string; // Chat UUID - type: ChatType; // Chat type - name: string | null; // Chat name - users: string[]; // Participants - messages: Message[]; // Empty initially -} -``` - -### Send Message (REST - for testing) -**Endpoint:** `POST /api/chats/message` - -**Authentication:** Required - -**Validation Rules:** -- `chatId`: valid UUID -- `message`: 1-2000 characters - -**Request Data:** -```typescript -{ - chatId: string; // Chat UUID - message: string; // Message content -} -``` - -**Response Data:** `Message` object - -### Archive Chat -**Endpoint:** `POST /api/chats/archive/{chatId}` - -**Authentication:** Required - -**URL Parameters:** -- `chatId`: Chat UUID (validated) - -**Response Data:** -```typescript -{ - success: boolean; - message: string; -} -``` - -### Restore Chat from Archive -**Endpoint:** `POST /api/chats/restore/{chatId}` - -**Authentication:** Required - -**URL Parameters:** -- `chatId`: Chat UUID (validated) - -**Response Data:** -```typescript -{ - success: boolean; - message: string; -} -``` - -### Get Archived Game Chats -**Endpoint:** `GET /api/chats/archived/game/{gameId}` - -**Authentication:** Required - -**URL Parameters:** -- `gameId`: Game UUID (validated) - -**Response Data:** Array of archived chat objects - ---- - -## Contact Management - -### Create Contact -**Endpoint:** `POST /api/contact` - -**Authentication:** Optional - -**Validation Rules:** -- `name`, `email`, `type`, `txt`: required fields -- `type`: must be 0-4 (ContactType enum) - -**Request Data:** -```typescript -{ - name: string; // Contact name (required) - email: string; // Contact email (required) - type: ContactType; // Contact type (0-4) - txt: string; // Message content (required) -} -``` - -**Response Data:** `ContactDto` - -**Error Responses:** -- `400`: Missing fields or invalid contact type -- `500`: Internal server error - ---- - -## Import/Export Functionality - -### Export Deck -**Endpoint:** `GET /api/deck-import-export/export/{deckId}` - -**Authentication:** Required (deck owner only) - -**URL Parameters:** -- `deckId`: Deck UUID - -**Response:** Binary .spr file download - -**Headers:** -- `Content-Type`: `application/octet-stream` -- `Content-Disposition`: `attachment; filename="deckname.spr"` - -### Import Deck -**Endpoint:** `POST /api/deck-import-export/import` - -**Authentication:** Required - -**Request:** Multipart form data -- `file`: .spr or JSON file (max 10MB) - -**Response Data:** -```typescript -{ - success: boolean; - message: string; - deckId: string; // Created deck ID -} -``` - ---- - -## Admin Endpoints - -All admin endpoints require authentication with admin role (UserState.ADMIN = 5). - -### User Management (Admin) - -#### Get Users (Paginated) - RECOMMENDED -**Endpoint:** `GET /api/admin/users/page/{from}/{to}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**URL Parameters:** -- `from`: Start index (0-based) -- `to`: End index (inclusive, max page size: 100) - -**Response Data:** -```typescript -{ - users: DetailUserDto[]; // Array of detailed user objects - pagination: { - from: number; // Start index - to: number; // End index - returned: number; // Actual returned count - totalCount: number; // Total user count - includeDeleted: boolean; // Include deleted flag - }; -} -``` - -#### Get User by ID (Admin) -**Endpoint:** `GET /api/admin/users/{userId}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** `DetailUserDto` or null - -#### Search Users (Admin) -**Endpoint:** `GET /api/admin/users/search/{searchTerm}` - -**URL Parameters:** -- `searchTerm`: Search term (2-100 characters) - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** Array of matching users - -#### Update User (Admin) -**Endpoint:** `PATCH /api/admin/users/{userId}` - -**Request Data:** Partial user fields to update - -**Response Data:** Updated `DetailUserDto` - -#### Deactivate User (Admin) -**Endpoint:** `POST /api/admin/users/{userId}/deactivate` - -**Response Data:** -```typescript -{ - message: string; - user: DetailUserDto; -} -``` - -#### Delete User (Admin) -**Endpoint:** `DELETE /api/admin/users/{userId}` - -**Response Data:** -```typescript -{ - message: string; -} -``` - -### Deck Management (Admin) - -#### Get Decks (Paginated, Admin) -**Endpoint:** `GET /api/admin/decks/page/{from}/{to}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** Same as regular deck pagination but unrestricted - -#### Get Deck by ID (Admin) -**Endpoint:** `GET /api/admin/decks/{id}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** `DetailDeckDto` - -#### Search Decks (Admin) -**Endpoint:** `GET /api/admin/decks/search/{searchTerm}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** Array of matching decks - -#### Hard Delete Deck (Admin) -**Endpoint:** `DELETE /api/admin/decks/{id}/hard` - -**Response Data:** -```typescript -{ - success: boolean; -} -``` - -### Organization Management (Admin) - -#### Create Organization (Admin) -**Endpoint:** `POST /api/admin/organizations` - -**Request Data:** Organization creation data - -**Response Data:** Created organization object - -#### Update Organization (Admin) -**Endpoint:** `PATCH /api/admin/organizations/{id}` - -**Request Data:** Partial organization fields to update - -**Response Data:** Updated organization object - -#### Get Organizations (Paginated, Admin) -**Endpoint:** `GET /api/admin/organizations/page/{from}/{to}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** Organization pagination with unrestricted access - -#### Get Organization by ID (Admin) -**Endpoint:** `GET /api/admin/organizations/{id}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** Organization object - -#### Search Organizations (Admin) -**Endpoint:** `GET /api/admin/organizations/search/{searchTerm}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** Array of matching organizations - -#### Soft Delete Organization (Admin) -**Endpoint:** `DELETE /api/admin/organizations/{id}` - -**Response Data:** -```typescript -{ - success: boolean; -} -``` - -#### Hard Delete Organization (Admin) -**Endpoint:** `DELETE /api/admin/organizations/{id}/hard` - -**Response Data:** -```typescript -{ - success: boolean; -} -``` - -### Chat Management (Admin) - -#### Get Chats (Paginated, Admin) -**Endpoint:** `GET /api/admin/chats/page/{from}/{to}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** -```typescript -{ - chats: DetailChatDto[]; - pagination: { - from: number; - to: number; - returned: number; - totalCount: number; - includeDeleted: boolean; - }; -} -``` - -#### Get Chat by ID (Admin) -**Endpoint:** `GET /api/admin/chats/{id}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** `DetailChatDto` - -### Contact Management (Admin) - -#### Get Contacts (Paginated, Admin) -**Endpoint:** `GET /api/admin/contacts/page/{from}/{to}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** Contact pagination with full access - -#### Get Contact by ID (Admin) -**Endpoint:** `GET /api/admin/contacts/{id}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** `ContactDto` - -#### Search Contacts (Admin) -**Endpoint:** `GET /api/admin/contacts/search/{searchTerm}` - -**Query Parameters:** -- `includeDeleted`: boolean (default: false) - -**Response Data:** Array of matching contacts - -#### Respond to Contact (Admin) -**Endpoint:** `PUT /api/admin/contacts/{id}/respond` - -**Request Data:** -```typescript -{ - adminResponse: string; // Admin response (required) - sendEmail?: boolean; // Send email to contact (optional) - language?: string; // Response language (optional) -} -``` - -**Response Data:** -```typescript -{ - success: boolean; - message: string; - contact: ContactDto; - emailSent: boolean; - emailError: string | null; -} -``` - -#### Resend Contact Email (Admin) -**Endpoint:** `POST /api/admin/contacts/{id}/resend-email` - -**Request Data:** -```typescript -{ - language?: string; // Email language (optional) -} -``` - -**Response Data:** -```typescript -{ - success: boolean; - message: string; -} -``` - -#### Soft Delete Contact (Admin) -**Endpoint:** `DELETE /api/admin/contacts/{id}` - -**Response Data:** -```typescript -{ - success: boolean; -} -``` - -#### Hard Delete Contact (Admin) -**Endpoint:** `DELETE /api/admin/contacts/{id}/hard` - -**Response Data:** -```typescript -{ - success: boolean; -} -``` - -### Import/Export (Admin) - -#### Import Deck from JSON (Admin) -**Endpoint:** `POST /api/admin/decks/import` - -**Request:** Multipart form data -- `file`: JSON file (max 10MB) - -**Response Data:** -```typescript -{ - success: boolean; - message: string; - deckId: string; -} -``` - -#### Export Deck as JSON (Admin) -**Endpoint:** `GET /api/admin/decks/{deckId}/export` - -**Response:** JSON file download - -**Headers:** -- `Content-Type`: `application/json` -- `Content-Disposition`: `attachment; filename="deckname.json"` - ---- - -## Error Handling - -### Standard Error Response Format -```typescript -{ - error: string; // Error message - details?: string; // Additional details (development only) - timestamp?: string; // Error timestamp -} -``` - -### HTTP Status Codes -- `200` - Success -- `201` - Created -- `204` - No Content -- `400` - Bad Request (validation error) -- `401` - Unauthorized (authentication required) -- `403` - Forbidden (insufficient permissions) -- `404` - Not Found -- `409` - Conflict (duplicate data) -- `500` - Internal Server Error -- `503` - Service Unavailable - -### Common Error Scenarios - -**Authentication Errors:** -```typescript -// Missing or invalid token -{ error: "Authentication required" } - -// Account state restrictions -{ error: "Please verify your email address" } -{ error: "Account has been deactivated" } - -// Admin access required -{ error: "Admin access required" } -``` - -**Validation Errors:** -```typescript -// Missing required fields -{ error: "Missing required fields: username, password" } - -// Invalid field length -{ error: "Username must be between 3 and 50 characters" } - -// Invalid parameters -{ error: "Invalid page parameters. \"from\" and \"to\" must be valid numbers with to >= from >= 0" } - -// Invalid file type -{ error: "Only JSON and .spr files are allowed" } -``` - -**Business Logic Errors:** -```typescript -// Ownership restrictions -{ error: "Access denied - you can only export your own decks" } - -// Feature restrictions -{ error: "Premium subscription required to create groups" } - -// Duplicate data -{ error: "Deck with this name already exists" } -{ error: "Username or email already exists" } -``` - ---- - -## WebSocket Real-Time Communication - -### Connection & Authentication - -Connect to WebSocket server with JWT authentication: - -```typescript -import io from 'socket.io-client'; - -// Option 1: JWT token in auth -const socket = io('http://localhost:3000', { - auth: { - token: 'your-jwt-token' - } -}); - -// Option 2: Cookie authentication -const socket = io('http://localhost:3000', { - withCredentials: true -}); -``` - -### Connection Events -```typescript -// Connection successful -socket.on('connect', () => { - console.log('Connected to WebSocket server'); -}); - -// Authentication failed -socket.on('connect_error', (error) => { - console.error('Connection failed:', error.message); -}); - -// Disconnected -socket.on('disconnect', (reason) => { - console.log('Disconnected:', reason); -}); - -// General errors -socket.on('error', (error: { message: string }) => { - console.error('WebSocket error:', error.message); -}); -``` - -### Chat Management Events - -**Initial Chat List:** -```typescript -// Automatically sent on connection -socket.on('chats:list', (chats: Array<{ - id: string; - type: ChatType; - name: string | null; - users: string[]; - lastActivity: Date | null; - isArchived: boolean; -}>) => { - // Update chat list in UI -}); -``` - -**Join/Leave Chat:** -```typescript -// Join a chat room -socket.emit('chat:join', { chatId: 'chat-uuid' }); - -// Confirmation of joining -socket.on('chat:joined', (data: { - chatId: string; - messages: Message[]; - users: string[]; -}) => { - // Load chat messages -}); - -// Leave a chat room -socket.emit('chat:leave', { chatId: 'chat-uuid' }); - -// Confirmation of leaving -socket.on('chat:left', (data: { chatId: string }) => { - // Update UI -}); -``` - -### Real-time Messaging - -**Send/Receive Messages:** -```typescript -// Send message -socket.emit('message:send', { - chatId: 'chat-uuid', - message: 'Hello everyone!' -}); - -// Receive message -socket.on('message:received', (data: { - chatId: string; - message: Message; - senderInfo?: { - username: string; - fname: string; - lname: string; - }; -}) => { - // Add message to chat UI -}); - -// Message sent confirmation -socket.on('message:sent', (data: { - chatId: string; - messageId: string; - timestamp: Date; -}) => { - // Update UI -}); -``` - -**Rate Limiting:** 100 messages per user per minute - -### Chat Creation Events - -**Create Group Chat (Premium Only):** -```typescript -// Create group -socket.emit('group:create', { - name: 'Study Group', - userIds: ['user-uuid-1', 'user-uuid-2'] -}); - -// Group created -socket.on('group:created', (data: { - chat: { - id: string; - type: 'group'; - name: string; - users: string[]; - createdBy: string; - }; -}) => { - // Add to chat list -}); - -// Creation failed -socket.on('group:creation:failed', (data: { - error: string; -}) => { - // Show error -}); -``` - -**Create Direct Chat:** -```typescript -// Create or get direct chat -socket.emit('chat:direct', { - targetUserId: 'user-uuid' -}); - -// Chat created -socket.on('chat:direct:created', (data: { - chat: { - id: string; - type: 'direct'; - users: string[]; - }; -}) => { - // Add to list -}); - -// Chat already exists -socket.on('chat:direct:exists', (data: { - chatId: string; -}) => { - // Navigate to existing chat -}); -``` - -**Create Game Chat:** -```typescript -// Create game chat -socket.emit('game:chat:create', { - gameId: 'game-uuid', - gameName: 'Quiz Game #123', - playerIds: ['player-uuid-1', 'player-uuid-2'] -}); - -// Game chat created -socket.on('game:chat:created', (data: { - chat: { - id: string; - type: 'game'; - name: string; - gameId: string; - users: string[]; - }; -}) => { - // Show game chat -}); -``` - -### Chat History Management - -**Get Chat History:** -```typescript -// Request full history -socket.emit('chat:history', { chatId: 'chat-uuid' }); - -// Receive active chat history -socket.on('chat:history', (data: { - chatId: string; - messages: Message[]; - users: string[]; - type: ChatType; - name: string | null; -}) => { - // Display full history -}); - -// Receive archived chat history -socket.on('chat:history:archived', (data: { - chatId: string; - messages: Message[]; - isGameChat: boolean; - archiveDate: Date; -}) => { - // Display as read-only -}); -``` - -### Message Retention Rules -- **All chats**: Messages older than 2 weeks are deleted -- **Direct & Game chats**: Max 10 messages per user (FIFO) -- **Group chats**: Time limit only (no per-user limit) -- **Archive**: Inactive chats (30 minutes) are archived -- **Cleanup**: Archived messages cleaned after 4 weeks - -### Complete Implementation Example - -```typescript -// hooks/useWebSocket.ts -import { useEffect, useRef, useState } from 'react'; -import io, { Socket } from 'socket.io-client'; - -export const useWebSocket = (token: string | null) => { - const socketRef = useRef(null); - const [isConnected, setIsConnected] = useState(false); - const [chats, setChats] = useState([]); - - useEffect(() => { - if (!token) return; - - socketRef.current = io('http://localhost:3000', { - auth: { token }, - withCredentials: true - }); - - const socket = socketRef.current; - - socket.on('connect', () => setIsConnected(true)); - socket.on('connect_error', () => setIsConnected(false)); - socket.on('disconnect', () => setIsConnected(false)); - - socket.on('chats:list', (chatList) => { - setChats(chatList); - }); - - socket.on('message:received', (data) => { - setChats(prev => prev.map(chat => - chat.id === data.chatId - ? { ...chat, messages: [...chat.messages, data.message] } - : chat - )); - }); - - return () => { - socket.disconnect(); - }; - }, [token]); - - const sendMessage = (chatId: string, message: string) => { - socketRef.current?.emit('message:send', { chatId, message }); - }; - - const joinChat = (chatId: string) => { - socketRef.current?.emit('chat:join', { chatId }); - }; - - const createDirectChat = (targetUserId: string) => { - socketRef.current?.emit('chat:direct', { targetUserId }); - }; - - const createGroup = (name: string, userIds: string[]) => { - socketRef.current?.emit('group:create', { name, userIds }); - }; - - return { - socket: socketRef.current, - isConnected, - chats, - sendMessage, - joinChat, - createDirectChat, - createGroup - }; -}; -``` - ---- - -This documentation provides a complete reference for all 50+ endpoints available in the SerpentRace backend API, with accurate data structures, validation rules, and implementation examples derived directly from the TypeScript source code. diff --git a/Documentations/PGADMIN_GUIDE.md b/Documentations/PGADMIN_GUIDE.md deleted file mode 100644 index 4b0e01d6..00000000 --- a/Documentations/PGADMIN_GUIDE.md +++ /dev/null @@ -1,117 +0,0 @@ -# pgAdmin Database Administration Guide - -## Access pgAdmin - -- **URL**: http://localhost:8080 -- **Email**: admin@serpentrace.dev -- **Password**: admin - -## Pre-configured Server - -The pgAdmin interface should have a pre-configured server named **"SerpentRace PostgreSQL Dev"** in the "Development" group. - -## Manual Server Configuration (If Needed) - -If the server is not automatically configured, add it manually: - -### Server Details -- **Name**: SerpentRace PostgreSQL Dev -- **Host**: postgres (or localhost if connecting from outside Docker) -- **Port**: 5432 -- **Database**: serpentrace -- **Username**: postgres -- **Password**: postgres - -### Steps to Add Server Manually - -1. Right-click on "Servers" in the left panel -2. Select "Register" > "Server..." -3. Fill in the "General" tab: - - Name: `SerpentRace PostgreSQL Dev` - - Server group: `Development` -4. Fill in the "Connection" tab: - - Host name/address: `postgres` - - Port: `5432` - - Maintenance database: `serpentrace` - - Username: `postgres` - - Password: `postgres` -5. Click "Save" - -## Common Database Operations - -### View Tables -1. Expand the server connection -2. Expand "Databases" > "serpentrace" -3. Expand "Schemas" > "public" -4. Expand "Tables" - -### Run SQL Queries -1. Right-click on the database name -2. Select "Query Tool" -3. Write your SQL queries in the editor -4. Click the "Execute" button or press F5 - -### View Data -1. Right-click on any table -2. Select "View/Edit Data" > "All Rows" - -## Troubleshooting - -### Connection Issues -- Ensure Docker containers are running: `docker ps` -- Check container logs: `docker logs serpentrace-postgres-dev` -- Test connections: `npm run test:connections` - -### Authentication Failed -- Verify the password is correct: `postgres` -- Check if you're using the correct hostname: `postgres` (inside Docker) vs `localhost` (outside Docker) - -### Server Not Appearing -- Restart pgAdmin container: - ```bash - docker-compose -f docker-compose.dev.yml restart pgadmin - ``` -- Clear browser cache and reload - -## Development Tips - -### Useful SQL Queries - -```sql --- List all tables -SELECT table_name FROM information_schema.tables -WHERE table_schema = 'public'; - --- Check database size -SELECT pg_size_pretty(pg_database_size('serpentrace')); - --- View active connections -SELECT * FROM pg_stat_activity WHERE datname = 'serpentrace'; - --- Check migration status (if using TypeORM) -SELECT * FROM migrations ORDER BY timestamp DESC; -``` - -### Database Backup -1. Right-click on database name -2. Select "Backup..." -3. Choose format (Custom recommended for pgAdmin restore) -4. Set filename and location -5. Click "Backup" - -### Database Restore -1. Right-click on "Databases" -2. Select "Restore..." -3. Choose the backup file -4. Configure options as needed -5. Click "Restore" - -## Security Notes - -⚠️ **Development Only**: The current configuration uses default credentials and is intended for development only. For production: - -- Use strong, unique passwords -- Enable SSL connections -- Restrict network access -- Use environment variables for credentials -- Enable authentication and authorization features diff --git a/FIXES_APPLIED.md b/FIXES_APPLIED.md deleted file mode 100644 index 8d11a651..00000000 --- a/FIXES_APPLIED.md +++ /dev/null @@ -1,217 +0,0 @@ -# 🔧 Game Fixes Applied - November 19, 2025 - -## Issues Fixed - -### 1. ✅ Cannot Answer Card Questions -**Problem**: Card modal wasn't receiving data properly from backend -**Root Cause**: Backend sends `game:card-drawn-self` event with nested structure `{ cardData: {...}, timeLimit: 60 }` but frontend was trying to access fields directly -**Solution**: -- Updated `handleCardDrawn` in GameScreen.jsx to properly extract `cardData` from nested structure -- Added support for `hint` field -- Properly handles both `game:card-drawn` and `game:card-drawn-self` events - -**Files Modified**: -- `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx` (lines 249-263) - -```javascript -const handleCardDrawn = (data) => { - // Backend sends cardData nested in game:card-drawn-self event - const cardData = data.cardData || data; - setCurrentCard({ - id: cardData.cardId || cardData.id, - type: cardData.cardType || cardData.type, - question: cardData.question || cardData.text || cardData.statement, - answerOptions: cardData.answerOptions || cardData.options || [], - correctAnswer: cardData.correctAnswer, - hint: cardData.hint, - points: cardData.points || 0, - timeLimit: data.timeLimit || cardData.timeLimit || 60 - }) - setIsCardModalOpen(true) -} -``` - ---- - -### 2. ✅ Player Turn Indicator Not Working -**Problem**: Turn indicator wasn't updating properly -**Root Cause**: Frontend didn't know which player was the current user to compare with `gameState.currentPlayer` -**Solution**: -- Added `playerIdentifier` state to GameWebSocketContext -- Decode gameToken on connect to extract `userId` or `playerName` -- Added `isMyTurn` computed value that compares `gameState.currentPlayer` with `playerIdentifier` - -**Files Modified**: -- `SerpentRace_Frontend/src/contexts/GameWebSocketContext.jsx` (lines 16, 57-62, 88-97, 488-489) - -```javascript -// In GameWebSocketContext -const [playerIdentifier, setPlayerIdentifier] = useState(null); - -// Decode token to get player identifier -try { - const payload = JSON.parse(atob(gameToken.split('.')[1])); - const identifier = payload.userId || payload.playerName; - setPlayerIdentifier(identifier); - log('🎮 Player identifier:', identifier); -} catch (err) { - logError('Failed to decode game token:', err); -} - -// Check if it's the current player's turn -const isMyTurn = useMemo(() => { - if (!gameState?.currentPlayer || !playerIdentifier) return false; - return gameState.currentPlayer === playerIdentifier; -}, [gameState?.currentPlayer, playerIdentifier]); -``` - ---- - -### 3. ✅ Current Player Name Not Shown in Indicator -**Problem**: Turn indicator only showed "Betöltés..." or player ID instead of player name -**Root Cause**: Inconsistent player ID format (some by `userId`, some by `playerName`) -**Solution**: -- Updated player lookup to check multiple possible ID formats -- Highlights current player name in green when it's your turn -- Shows "← Te vagy!" (It's you!) indicator next to your name - -**Files Modified**: -- `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx` (lines 470-476) - -```javascript -{currentTurn && ( -
- 🎯 Köron: - {players.find(p => p.id === currentTurn || p.playerName === currentTurn || p.name === currentTurn)?.name || currentTurn || 'Betöltés...'} - - {isMyTurn && ← Te vagy!} -
-)} -``` - ---- - -### 4. ✅ Dice Shown Even When Not Player's Turn -**Problem**: Dice was always interactive regardless of whose turn it was -**Root Cause**: No turn validation on dice display -**Solution**: -- Added conditional rendering based on `isMyTurn` flag -- When it's your turn: Shows green pulsing text "🎯 A te köröd! Kattints a kockára dobáshoz!" -- When it's NOT your turn: Shows gray text "⏳ Várd meg a köröd..." and dice is disabled with 50% opacity and `pointer-events-none` - -**Files Modified**: -- `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx` (lines 609-625) - -```javascript -{isMyTurn ? ( - <> -

- 🎯 A te köröd! Kattints a kockára dobáshoz! -

- - -) : ( - <> -

- ⏳ Várd meg a köröd... -

-
- -
- -)} -``` - ---- - -## Additional Improvements - -### Debug Panel Enhancement -Added debug information to help verify turn system: -- **🆔 My ID**: Shows current player's identifier (userId or playerName) -- **✅ Is My Turn**: Shows YES/NO to quickly verify turn detection - -**Files Modified**: -- `SerpentRace_Frontend/src/pages/Game/GameScreen.jsx` (lines 643-644) - ---- - -## Technical Details - -### Token Structure -The gameToken is a JWT containing: -```json -{ - "gameId": "uuid", - "gameCode": "ABC123", - "playerName": "Player1", - "isAuthenticated": true/false, - "userId": "uuid" // only for authenticated players -} -``` - -### Player Identification Logic -Backend uses: `playerIdentifier = socket.userId || socket.playerName` -Frontend now extracts: `payload.userId || payload.playerName` from decoded token - -This ensures both authenticated users (with userId) and guest players (with only playerName) work correctly. - ---- - -## Testing Checklist - -### ✅ Card System -- [ ] Draw a card and verify modal opens with question -- [ ] Verify answer options display correctly (for quiz cards) -- [ ] Submit answer and verify it's sent to backend -- [ ] Check hint displays if available -- [ ] Verify timer countdown works - -### ✅ Turn System -- [ ] Game starts and first player sees "🎯 A te köröd!" -- [ ] Other players see "⏳ Várd meg a köröd..." -- [ ] Turn indicator shows correct player name -- [ ] "← Te vagy!" appears next to your name when it's your turn -- [ ] Name is highlighted in green when it's your turn - -### ✅ Dice Control -- [ ] Dice is interactive (clickable) only on your turn -- [ ] Dice is grayed out and disabled when not your turn -- [ ] Text changes from green "A te köröd!" to gray "Várd meg a köröd..." - -### ✅ Multi-Player Testing -- [ ] Test with 2+ authenticated players -- [ ] Test with guest players (no login) -- [ ] Test with mix of authenticated and guest players -- [ ] Verify turn rotation works correctly -- [ ] Each player can only act on their turn - ---- - -## Files Modified Summary - -1. **SerpentRace_Frontend/src/contexts/GameWebSocketContext.jsx** - - Added `playerIdentifier` state - - Added token decoding on connect - - Added `isMyTurn` computed value - - Exported new values in context - -2. **SerpentRace_Frontend/src/pages/Game/GameScreen.jsx** - - Fixed card modal data extraction - - Updated turn indicator with name lookup - - Added turn-based dice control - - Added debug info for turn tracking - - Imported `isMyTurn` and `playerIdentifier` from context - ---- - -## Compilation Status - -✅ **No TypeScript/JavaScript errors** -✅ **All changes backwards compatible** -✅ **Ready for testing** - ---- - -**Last Updated**: November 19, 2025 -**Status**: All 4 issues resolved and tested for compilation errors diff --git a/Javitás.txt b/Javitás.txt deleted file mode 100644 index 13b10841..00000000 --- a/Javitás.txt +++ /dev/null @@ -1,62 +0,0 @@ -Javitás - -Deckeck: - - Következmény csak szerencse kártyánál - - Egy fajta következmény (/lap, automatikusan kerül végrehajtásra) - - Hibás kártya pakli mentésekor is törlödjön - - extra kör, kimarad bármennyi 1-től 5-ig - - megnyitás, szerkesztés, adatok betöltése - - Mentési ADATOK Csekkolása | ZSOLA - - Closer option - -navbar: - - tegnapiak - -TEGNAPI HIBÁK JAVÍTÁSA: - - kapcs fel routing - - navbar széthúz - - footer kapcsolat - - navabar gomboksorrend - - vagy kontat vagy kapcsolat - - navbar bejelent - - navbar layout finomít - - palki info get - - -GET /ap/decks/page/:from/:to (0-49) 50db (50-99) 50db ... (0-29) 30db => (30-59) 30db - - from: (oldalsz-1)*dbsz (pl: (1-1)*30=0; (2-1)*30=30) - - to: (oldalsz*dbsz) - 1 (pl: (1*30)-1=29; (2*30)-1 =59) - -email verifikáció: - - verify-email/:code => Email címe hitelesítés alatt: stb - - ha sikeres => login => toastify => email címe hitelesítve - - ha sikertelen => home/register => toastify/pushup => sikertelen vegye fel velünk a kapcsolatot - - - POST api/users/verify-email/:code <= BACKEND URI - - - -HOLNAP ESTE 19:00 => Jó lenne, ha ezek megvannak -HOLNAPTÓL => JÁTÉK => SOCKET IO működése - - -Mobil nézet: - - landing page - - navbar - - footer - - pakli fő nézet => bar - - pakli összerakás és szerkesztés - - bejelentkezés - - regisztráció - -User felület: - - Saját adatok lekérése - - Saját adatok módosítása: - - email-cím - - telefonszám - - jelszó - - felhasználó név - - Saját profil törlése - - Elfelelejtett jelszó - - Kérése => email-cím alapján => POST /api/users/forgot-password - - password-reset/:token => POST /api/users/reset-password diff --git a/README.md b/README.md deleted file mode 100644 index a0fccc0f..00000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# SerpentRace - -- Frontend: React (Vite) -- Backend: Node.js (Express.js) - -## Development Commands - -### Start with File Watchers (Recommended) -```bash -# Windows -.\docker-manage.bat dev:watch - -# Linux/Mac -./docker-manage.sh dev:watch -``` -Automatically syncs file changes and rebuilds containers when needed. - -### Traditional Start -```bash -# Windows -.\docker-manage.bat dev:start - -# Linux/Mac -./docker-manage.sh dev:start -``` - -## Documentation -- [Docker Watcher Guide](./Documentations/DOCKER_WATCHER_GUIDE.md) - Comprehensive guide for file watching functionality \ No newline at end of file diff --git a/SerpentRace_Backend/.dockerignore b/SerpentRace_Backend/.dockerignore deleted file mode 100644 index fb8f96bb..00000000 --- a/SerpentRace_Backend/.dockerignore +++ /dev/null @@ -1,27 +0,0 @@ -node_modules -npm-debug.log -.git -.gitignore -README.md -.env -.nyc_output -coverage -.coverage -.coverage.* -.cache -logs -*.log -.DS_Store -.vscode -.idea -*.swp -*.swo -dist -build -.next -.nuxt -.vuepress/dist -.serverless -.fusebox/ -.dynamodb/ -.tern-port diff --git a/SerpentRace_Backend/.env.dev b/SerpentRace_Backend/.env.dev deleted file mode 100644 index 7f280d5f..00000000 --- a/SerpentRace_Backend/.env.dev +++ /dev/null @@ -1,41 +0,0 @@ -# Development Environment Variables for Local Build -# These are used when running build scripts outside of Docker containers - -NODE_ENV=development -PORT=3000 - -# Database Configuration (Docker containers) -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=postgres - -# Redis Configuration (Docker containers) -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_URL=redis://localhost:6379 - -# JWT Configuration -JWT_SECRET=dev_jwt_secret_change_in_production -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d - -# MinIO Configuration (Docker containers) -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_ACCESS_KEY=serpentrace -MINIO_SECRET_KEY=serpentrace123! -MINIO_USE_SSL=false - -# Board Generation Configuration -MAX_SPECIAL_FIELDS_PERCENTAGE=67 -MAX_GENERATION_TIME_SECONDS=20 -GENERATION_ERROR_TOLERANCE=15 - -# EMAIL SERVICE CONFIGURATION -EMAIL_HOST=smtp.gmail.com -EMAIL_PORT=587 -EMAIL_USER=your_email@domain.com -EMAIL_PASS=your_email_password -EMAIL_FROM=noreply@serpentrace.com \ No newline at end of file diff --git a/SerpentRace_Backend/.env.example b/SerpentRace_Backend/.env.example deleted file mode 100644 index cd5d9d7c..00000000 --- a/SerpentRace_Backend/.env.example +++ /dev/null @@ -1,62 +0,0 @@ -# ============================================== -# SerpentRace Backend Environment Configuration -# ============================================== -# Copy this file to .env and fill in your values - -# APPLICATION CONFIGURATION -NODE_ENV=development -PORT=3000 -APP_BASE_URL=http://localhost:3000 - -# DATABASE CONFIGURATION (PostgreSQL) -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=your_db_password - -# REDIS CONFIGURATION -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_URL=redis://localhost:6379 - -# JWT AUTHENTICATION CONFIGURATION -JWT_SECRET=your-super-secure-secret-key-here -JWT_REFRESH_SECRET=your-super-secure-refresh-secret-key-here - -# Access Token Expiry (choose ONE option, priority order listed): -JWT_ACCESS_TOKEN_EXPIRY=1800 # Seconds (recommended for production) -# JWT_ACCESS_TOKEN_EXPIRATION=30m # Duration string (user-friendly) -# JWT_EXPIRY=1800 # Legacy: seconds -# JWT_EXPIRATION=30m # Legacy: duration string - -# Refresh Token Expiry (choose ONE option, priority order listed): -JWT_REFRESH_TOKEN_EXPIRY=604800 # Seconds (7 days) -# JWT_REFRESH_TOKEN_EXPIRATION=7d # Duration string (recommended) -# JWT_REFRESH_EXPIRATION=7d # Legacy: duration string - -# Cookie Names (optional) -JWT_COOKIE_NAME=auth_token -JWT_REFRESH_COOKIE_NAME=refresh_token - -# Legacy JWT Configuration (deprecated - use above options) -# JWT_EXPIRY=86400 -# JWT_EXPIRATION=24h -GAME_TOKEN_EXPIRY=86400 - -# EMAIL SERVICE CONFIGURATION -EMAIL_HOST=smtp.gmail.com -EMAIL_PORT=587 -EMAIL_USER=your_email@domain.com -EMAIL_PASS=your_email_password -EMAIL_FROM=noreply@serpentrace.com - -# CHAT SYSTEM CONFIGURATION -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 -CHAT_MAX_MESSAGES_PER_USER=100 -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# GAME CONFIGURATION -MAX_SPECIAL_FIELDS_PERCENTAGE=67 -MAX_GENERATION_TIME_SECONDS=20 -GENERATION_ERROR_TOLERANCE=15 diff --git a/SerpentRace_Backend/.gitignore b/SerpentRace_Backend/.gitignore deleted file mode 100644 index c8d513b6..00000000 --- a/SerpentRace_Backend/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -./dist/* -./node_modules/* -./Archive_*/* -./Archive_* -./logs/* diff --git a/SerpentRace_Backend/assets/Logo.png b/SerpentRace_Backend/assets/Logo.png deleted file mode 100644 index 480d8c5d..00000000 Binary files a/SerpentRace_Backend/assets/Logo.png and /dev/null differ diff --git a/SerpentRace_Backend/jest.config.js b/SerpentRace_Backend/jest.config.js deleted file mode 100644 index 929a0400..00000000 --- a/SerpentRace_Backend/jest.config.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/tests', '/src'], - testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], - transform: { - '^.+\\.ts$': 'ts-jest', - }, - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/**/*.d.ts', - '!src/Api/index.ts', - '!src/Infrastructure/ormconfig.ts', - '!src/search-demo.ts' - ], - coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov', 'html'], - moduleFileExtensions: ['ts', 'js', 'json'], - setupFilesAfterEnv: ['/tests/setup.ts'], - testTimeout: 10000, - setupFiles: ['/tests/jest.setup.ts'], - verbose: true, - moduleNameMapper: { - '^@/(.*)$': '/src/$1' - }, - resolver: undefined, - moduleDirectories: ['node_modules', '/src', '/tests'] -}; diff --git a/SerpentRace_Backend/language-test.js b/SerpentRace_Backend/language-test.js deleted file mode 100644 index 4895e213..00000000 --- a/SerpentRace_Backend/language-test.js +++ /dev/null @@ -1,29 +0,0 @@ -// Quick test to demonstrate the language detection functionality -const { extractLanguageFromAcceptHeader } = require('./src/Api/contactRouter.js'); - -// Test cases to demonstrate Accept-Language parsing -const testCases = [ - 'en-US,en;q=0.9', - 'hu,en;q=0.9', - 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7', - 'hu-HU,hu;q=0.9,en-US;q=0.8', - 'fr-FR,fr;q=0.9,en;q=0.8', - 'es,en-US;q=0.9,en;q=0.8', - 'invalid-header', - '' -]; - -console.log('Testing Accept-Language header parsing:\n'); - -testCases.forEach(header => { - const result = extractLanguageFromAcceptHeader(header); - console.log(`Header: "${header}" -> Language: ${result}`); -}); - -console.log('\n✅ Multi-language system is working correctly!'); -console.log('\nFeatures implemented:'); -console.log('- Accept-Language header parsing with quality values'); -console.log('- Support for EN, HU, DE templates'); -console.log('- Custom header detection (X-Language, X-Region, X-Locale)'); -console.log('- Fallback to English for unsupported languages'); -console.log('- Professional email templates in all three languages'); diff --git a/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js b/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js deleted file mode 100644 index 0d7d9dab..00000000 --- a/SerpentRace_Backend/node_modules/jest-runner/build/testWorker.js +++ /dev/null @@ -1,513 +0,0 @@ - -/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.483\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.483/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ - -/*! - * /** - * * Copyright (c) Meta Platforms, Inc. and affiliates. - * * - * * This source code is licensed under the MIT license found in the - * * LICENSE file in the root directory of this source tree. - * * / - */ -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ "./src/runTest.ts": -/***/ ((__unused_webpack_module, exports) => { - - - -Object.defineProperty(exports, "__esModule", ({ - value: true -})); -exports["default"] = runTest; -function _nodeVm() { - const data = require("node:vm"); - _nodeVm = function () { - return data; - }; - return data; -} -function _chalk() { - const data = _interopRequireDefault(require("chalk")); - _chalk = function () { - return data; - }; - return data; -} -function fs() { - const data = _interopRequireWildcard(require("graceful-fs")); - fs = function () { - return data; - }; - return data; -} -function sourcemapSupport() { - const data = _interopRequireWildcard(require("source-map-support")); - sourcemapSupport = function () { - return data; - }; - return data; -} -function _console() { - const data = require("@jest/console"); - _console = function () { - return data; - }; - return data; -} -function _transform() { - const data = require("@jest/transform"); - _transform = function () { - return data; - }; - return data; -} -function docblock() { - const data = _interopRequireWildcard(require("jest-docblock")); - docblock = function () { - return data; - }; - return data; -} -function _jestLeakDetector() { - const data = _interopRequireDefault(require("jest-leak-detector")); - _jestLeakDetector = function () { - return data; - }; - return data; -} -function _jestMessageUtil() { - const data = require("jest-message-util"); - _jestMessageUtil = function () { - return data; - }; - return data; -} -function _jestResolve() { - const data = require("jest-resolve"); - _jestResolve = function () { - return data; - }; - return data; -} -function _jestUtil() { - const data = require("jest-util"); - _jestUtil = function () { - return data; - }; - return data; -} -function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } -function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -// eslint-disable-next-line @typescript-eslint/consistent-type-imports - -function freezeConsole(testConsole, config) { - // @ts-expect-error: `_log` is `private` - we should figure out some proper API here - testConsole._log = function fakeConsolePush(_type, message) { - const error = new (_jestUtil().ErrorWithStack)(`${_chalk().default.red(`${_chalk().default.bold('Cannot log after tests are done.')} Did you forget to wait for something async in your test?`)}\nAttempted to log "${message}".`, fakeConsolePush); - const formattedError = (0, _jestMessageUtil().formatExecError)(error, config, { - noStackTrace: false - }, undefined, true); - process.stderr.write(`\n${formattedError}\n`); - process.exitCode = 1; - }; -} - -// Keeping the core of "runTest" as a separate function (as "runTestInternal") -// is key to be able to detect memory leaks. Since all variables are local to -// the function, when "runTestInternal" finishes its execution, they can all be -// freed, UNLESS something else is leaking them (and that's why we can detect -// the leak!). -// -// If we had all the code in a single function, we should manually nullify all -// references to verify if there is a leak, which is not maintainable and error -// prone. That's why "runTestInternal" CANNOT be inlined inside "runTest". -async function runTestInternal(path, globalConfig, projectConfig, resolver, context, sendMessageToJest) { - const testSource = fs().readFileSync(path, 'utf8'); - const docblockPragmas = docblock().parse(docblock().extract(testSource)); - const customEnvironment = docblockPragmas['jest-environment']; - const loadTestEnvironmentStart = Date.now(); - let testEnvironment = projectConfig.testEnvironment; - if (customEnvironment) { - if (Array.isArray(customEnvironment)) { - throw new TypeError(`You can only define a single test environment through docblocks, got "${customEnvironment.join(', ')}"`); - } - testEnvironment = (0, _jestResolve().resolveTestEnvironment)({ - ...projectConfig, - // we wanna avoid webpack trying to be clever - requireResolveFunction: module => require.resolve(module), - testEnvironment: customEnvironment - }); - } - const cacheFS = new Map([[path, testSource]]); - const transformer = await (0, _transform().createScriptTransformer)(projectConfig, cacheFS); - const TestEnvironment = await transformer.requireAndTranspileModule(testEnvironment); - const testFramework = await transformer.requireAndTranspileModule(process.env.JEST_JASMINE === '1' ? require.resolve('jest-jasmine2') : projectConfig.testRunner); - const Runtime = (0, _jestUtil().interopRequireDefault)(projectConfig.runtime ? require(projectConfig.runtime) : require('jest-runtime')).default; - const consoleOut = globalConfig.useStderr ? process.stderr : process.stdout; - const consoleFormatter = (type, message) => (0, _console().getConsoleOutput)( - // 4 = the console call is buried 4 stack frames deep - _console().BufferedConsole.write([], type, message, 4), projectConfig, globalConfig); - let testConsole; - if (globalConfig.silent) { - testConsole = new (_console().NullConsole)(consoleOut, consoleOut, consoleFormatter); - } else if (globalConfig.verbose) { - testConsole = new (_console().CustomConsole)(consoleOut, consoleOut, consoleFormatter); - } else { - testConsole = new (_console().BufferedConsole)(); - } - let extraTestEnvironmentOptions; - const docblockEnvironmentOptions = docblockPragmas['jest-environment-options']; - if (typeof docblockEnvironmentOptions === 'string') { - extraTestEnvironmentOptions = JSON.parse(docblockEnvironmentOptions); - } - const environment = new TestEnvironment({ - globalConfig, - projectConfig: extraTestEnvironmentOptions ? { - ...projectConfig, - testEnvironmentOptions: { - ...projectConfig.testEnvironmentOptions, - ...extraTestEnvironmentOptions - } - } : projectConfig - }, { - console: testConsole, - docblockPragmas, - testPath: path - }); - const loadTestEnvironmentEnd = Date.now(); - if (typeof environment.getVmContext !== 'function') { - console.error(`Test environment found at "${testEnvironment}" does not export a "getVmContext" method, which is mandatory from Jest 27. This method is a replacement for "runScript".`); - process.exit(1); - } - const leakDetector = projectConfig.detectLeaks ? new (_jestLeakDetector().default)(environment) : null; - (0, _jestUtil().setGlobal)(environment.global, 'console', testConsole, 'retain'); - const runtime = new Runtime(projectConfig, environment, resolver, transformer, cacheFS, { - changedFiles: context.changedFiles, - collectCoverage: globalConfig.collectCoverage, - collectCoverageFrom: globalConfig.collectCoverageFrom, - coverageProvider: globalConfig.coverageProvider, - sourcesRelatedToTestsInChangedFiles: context.sourcesRelatedToTestsInChangedFiles - }, path, globalConfig); - let isTornDown = false; - const tearDownEnv = async () => { - if (!isTornDown) { - runtime.teardown(); - - // source-map-support keeps memory leftovers in `Error.prepareStackTrace` - (0, _nodeVm().runInContext)("Error.prepareStackTrace = () => '';", environment.getVmContext()); - sourcemapSupport().resetRetrieveHandlers(); - try { - await environment.teardown(); - } finally { - isTornDown = true; - } - } - }; - const start = Date.now(); - const setupFilesStart = Date.now(); - for (const path of projectConfig.setupFiles) { - const esm = runtime.unstable_shouldLoadAsEsm(path); - if (esm) { - await runtime.unstable_importModule(path); - } else { - const setupFile = runtime.requireModule(path); - if (typeof setupFile === 'function') { - await setupFile(); - } - } - } - const setupFilesEnd = Date.now(); - const sourcemapOptions = { - environment: 'node', - handleUncaughtExceptions: false, - retrieveSourceMap: source => { - const sourceMapSource = runtime.getSourceMaps()?.get(source); - if (sourceMapSource) { - try { - return { - map: JSON.parse(fs().readFileSync(sourceMapSource, 'utf8')), - url: source - }; - } catch {} - } - return null; - } - }; - - // For tests - runtime.requireInternalModule(require.resolve('source-map-support')).install(sourcemapOptions); - - // For runtime errors - sourcemapSupport().install(sourcemapOptions); - if (environment.global && environment.global.process && environment.global.process.exit) { - const realExit = environment.global.process.exit; - environment.global.process.exit = function exit(...args) { - const error = new (_jestUtil().ErrorWithStack)(`process.exit called with "${args.join(', ')}"`, exit); - const formattedError = (0, _jestMessageUtil().formatExecError)(error, projectConfig, { - noStackTrace: false - }, undefined, true); - process.stderr.write(formattedError); - return realExit(...args); - }; - } - - // if we don't have `getVmContext` on the env skip coverage - const collectV8Coverage = globalConfig.collectCoverage && globalConfig.coverageProvider === 'v8' && typeof environment.getVmContext === 'function'; - - // Node's error-message stack size is limited at 10, but it's pretty useful - // to see more than that when a test fails. - Error.stackTraceLimit = 100; - try { - await environment.setup(); - let result; - try { - if (collectV8Coverage) { - await runtime.collectV8Coverage(); - } - result = await testFramework(globalConfig, projectConfig, environment, runtime, path, sendMessageToJest); - } catch (error) { - // Access all stacks before uninstalling sourcemaps - let e = error; - while (typeof e === 'object' && e !== null && 'stack' in e) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - e.stack; - e = e?.cause; - } - throw error; - } finally { - if (collectV8Coverage) { - await runtime.stopCollectingV8Coverage(); - } - } - freezeConsole(testConsole, projectConfig); - const testCount = result.numPassingTests + result.numFailingTests + result.numPendingTests + result.numTodoTests; - const end = Date.now(); - const testRuntime = end - start; - result.perfStats = { - ...result.perfStats, - end, - loadTestEnvironmentEnd, - loadTestEnvironmentStart, - runtime: testRuntime, - setupFilesEnd, - setupFilesStart, - slow: testRuntime / 1000 > projectConfig.slowTestThreshold, - start - }; - result.testFilePath = path; - result.console = testConsole.getBuffer(); - result.skipped = testCount === result.numPendingTests; - result.displayName = projectConfig.displayName; - const coverage = runtime.getAllCoverageInfoCopy(); - if (coverage) { - const coverageKeys = Object.keys(coverage); - if (coverageKeys.length > 0) { - result.coverage = coverage; - } - } - if (collectV8Coverage) { - const v8Coverage = runtime.getAllV8CoverageInfoCopy(); - if (v8Coverage && v8Coverage.length > 0) { - result.v8Coverage = v8Coverage; - } - } - if (globalConfig.logHeapUsage) { - globalThis.gc?.(); - result.memoryUsage = process.memoryUsage().heapUsed; - } - await tearDownEnv(); - - // Delay the resolution to allow log messages to be output. - return await new Promise(resolve => { - setImmediate(() => resolve({ - leakDetector, - result - })); - }); - } finally { - await tearDownEnv(); - } -} -async function runTest(path, globalConfig, config, resolver, context, sendMessageToJest) { - const { - leakDetector, - result - } = await runTestInternal(path, globalConfig, config, resolver, context, sendMessageToJest); - if (leakDetector) { - // We wanna allow a tiny but time to pass to allow last-minute cleanup - await new Promise(resolve => setTimeout(resolve, 100)); - - // Resolve leak detector, outside the "runTestInternal" closure. - result.leaks = await leakDetector.isLeaking(); - } else { - result.leaks = false; - } - return result; -} - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The module cache -/******/ var __webpack_module_cache__ = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ // Check if module is in cache -/******/ var cachedModule = __webpack_module_cache__[moduleId]; -/******/ if (cachedModule !== undefined) { -/******/ return cachedModule.exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = __webpack_module_cache__[moduleId] = { -/******/ // no module.id needed -/******/ // no module.loaded needed -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -// This entry needs to be wrapped in an IIFE because it uses a non-standard name for the exports (exports). -(() => { -var exports = __webpack_exports__; - - -Object.defineProperty(exports, "__esModule", ({ - value: true -})); -exports.setup = setup; -exports.worker = worker; -function _exitX() { - const data = _interopRequireDefault(require("exit-x")); - _exitX = function () { - return data; - }; - return data; -} -function _jestHasteMap() { - const data = _interopRequireDefault(require("jest-haste-map")); - _jestHasteMap = function () { - return data; - }; - return data; -} -function _jestMessageUtil() { - const data = require("jest-message-util"); - _jestMessageUtil = function () { - return data; - }; - return data; -} -function _jestRuntime() { - const data = _interopRequireDefault(require("jest-runtime")); - _jestRuntime = function () { - return data; - }; - return data; -} -function _jestWorker() { - const data = require("jest-worker"); - _jestWorker = function () { - return data; - }; - return data; -} -var _runTest = _interopRequireDefault(__webpack_require__("./src/runTest.ts")); -function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -// Make sure uncaught errors are logged before we exit. -process.on('uncaughtException', err => { - if (err.stack) { - console.error(err.stack); - } else { - console.error(err); - } - (0, _exitX().default)(1); -}); -const formatError = error => { - if (typeof error === 'string') { - const { - message, - stack - } = (0, _jestMessageUtil().separateMessageFromStack)(error); - return { - message, - stack, - type: 'Error' - }; - } - return { - code: error.code || undefined, - message: error.message, - stack: error.stack, - type: 'Error' - }; -}; -const resolvers = new Map(); -const getResolver = config => { - const resolver = resolvers.get(config.id); - if (!resolver) { - throw new Error(`Cannot find resolver for: ${config.id}`); - } - return resolver; -}; -function setup(setupData) { - // Module maps that will be needed for the test runs are passed. - for (const { - config, - serializableModuleMap - } of setupData.serializableResolvers) { - const moduleMap = _jestHasteMap().default.getStatic(config).getModuleMapFromJSON(serializableModuleMap); - resolvers.set(config.id, _jestRuntime().default.createResolver(config, moduleMap)); - } -} -const sendMessageToJest = (eventName, args) => { - (0, _jestWorker().messageParent)([eventName, args]); -}; -async function worker({ - config, - globalConfig, - path, - context -}) { - try { - return await (0, _runTest.default)(path, globalConfig, config, getResolver(config), { - ...context, - changedFiles: context.changedFiles && new Set(context.changedFiles), - sourcesRelatedToTestsInChangedFiles: context.sourcesRelatedToTestsInChangedFiles && new Set(context.sourcesRelatedToTestsInChangedFiles) - }, sendMessageToJest); - } catch (error) { - throw formatError(error); - } -} -})(); - -module.exports = __webpack_exports__; -/******/ })() -; \ No newline at end of file diff --git a/SerpentRace_Backend/node_modules/jest/bin/jest.js b/SerpentRace_Backend/node_modules/jest/bin/jest.js deleted file mode 100644 index 44425d69..00000000 --- a/SerpentRace_Backend/node_modules/jest/bin/jest.js +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env node -/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.483\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.483/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ - - -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const importLocal = require('import-local'); - -if (!importLocal(__filename)) { - require('jest-cli/bin/jest'); -} diff --git a/SerpentRace_Backend/package-lock.json b/SerpentRace_Backend/package-lock.json deleted file mode 100644 index be1a5942..00000000 --- a/SerpentRace_Backend/package-lock.json +++ /dev/null @@ -1,10435 +0,0 @@ -{ - "name": "serpentrace_backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "serpentrace_backend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "bcrypt": "^6.0.0", - "cookie-parser": "^1.4.7", - "express": "^5.1.0", - "helmet": "^8.1.0", - "jsonwebtoken": "^9.0.2", - "minio": "^8.0.5", - "multer": "^2.0.2", - "nodemailer": "^7.0.5", - "pg": "^8.16.3", - "redis": "^5.8.1", - "sharp": "^0.34.4", - "socket.io": "^4.8.1", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", - "tsconfig-paths": "^4.2.0", - "typeorm": "^0.3.26", - "uuid": "^11.1.0", - "winston": "^3.17.0" - }, - "devDependencies": { - "@jest/globals": "^30.0.5", - "@types/bcrypt": "^6.0.0", - "@types/cookie-parser": "^1.4.9", - "@types/express": "^5.0.3", - "@types/jest": "^30.0.0", - "@types/jsonwebtoken": "^9.0.10", - "@types/multer": "^2.0.0", - "@types/node": "^24.3.3", - "@types/nodemailer": "^7.0.1", - "@types/pg": "^8.15.5", - "@types/redis": "^4.0.10", - "@types/socket.io": "^3.0.1", - "@types/socket.io-client": "^1.4.36", - "@types/supertest": "^6.0.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.8", - "@types/uuid": "^10.0.0", - "jest": "^30.0.5", - "nodemon": "^3.1.10", - "rimraf": "^5.0.10", - "socket.io-client": "^4.8.1", - "supertest": "^7.1.4", - "ts-jest": "^29.4.1", - "ts-node": "^10.9.2", - "typescript": "^5.9.2" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@ampproject/remapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" - } - }, - "node_modules/@apidevtools/openapi-schemas": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", - "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@apidevtools/swagger-methods": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", - "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", - "license": "MIT" - }, - "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", - "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" - }, - "peerDependencies": { - "openapi-types": ">=7" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.864.0.tgz", - "integrity": "sha512-pwn4/3bs7ccucS9sYpMbzptEhEFQQy8TXtmKNzmyY7OIDBGTiJrxsWYDTULO4nxsMmGXi39mSEowlK4QUCyC+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/credential-provider-node": "3.864.0", - "@aws-sdk/middleware-host-header": "3.862.0", - "@aws-sdk/middleware-logger": "3.862.0", - "@aws-sdk/middleware-recursion-detection": "3.862.0", - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/region-config-resolver": "3.862.0", - "@aws-sdk/signature-v4-multi-region": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@aws-sdk/util-user-agent-browser": "3.862.0", - "@aws-sdk/util-user-agent-node": "3.864.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.864.0.tgz", - "integrity": "sha512-THiOp0OpQROEKZ6IdDCDNNh3qnNn/kFFaTSOiugDpgcE5QdsOxh1/RXq7LmHpTJum3cmnFf8jG59PHcz9Tjnlw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/middleware-host-header": "3.862.0", - "@aws-sdk/middleware-logger": "3.862.0", - "@aws-sdk/middleware-recursion-detection": "3.862.0", - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/region-config-resolver": "3.862.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@aws-sdk/util-user-agent-browser": "3.862.0", - "@aws-sdk/util-user-agent-node": "3.864.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.864.0.tgz", - "integrity": "sha512-LFUREbobleHEln+Zf7IG83lAZwvHZG0stI7UU0CtwyuhQy5Yx0rKksHNOCmlM7MpTEbSCfntEhYi3jUaY5e5lg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@aws-sdk/xml-builder": "3.862.0", - "@smithy/core": "^3.8.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/signature-v4": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-utf8": "^4.0.0", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.864.0.tgz", - "integrity": "sha512-StJPOI2Rt8UE6lYjXUpg6tqSZaM72xg46ljPg8kIevtBAAfdtq9K20qT/kSliWGIBocMFAv0g2mC0hAa+ECyvg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.864.0.tgz", - "integrity": "sha512-E/RFVxGTuGnuD+9pFPH2j4l6HvrXzPhmpL8H8nOoJUosjx7d4v93GJMbbl1v/fkDLqW9qN4Jx2cI6PAjohA6OA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/property-provider": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-stream": "^4.2.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.864.0.tgz", - "integrity": "sha512-PlxrijguR1gxyPd5EYam6OfWLarj2MJGf07DvCx9MAuQkw77HBnsu6+XbV8fQriFuoJVTBLn9ROhMr/ROAYfUg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/credential-provider-env": "3.864.0", - "@aws-sdk/credential-provider-http": "3.864.0", - "@aws-sdk/credential-provider-process": "3.864.0", - "@aws-sdk/credential-provider-sso": "3.864.0", - "@aws-sdk/credential-provider-web-identity": "3.864.0", - "@aws-sdk/nested-clients": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.864.0.tgz", - "integrity": "sha512-2BEymFeXURS+4jE9tP3vahPwbYRl0/1MVaFZcijj6pq+nf5EPGvkFillbdBRdc98ZI2NedZgSKu3gfZXgYdUhQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.864.0", - "@aws-sdk/credential-provider-http": "3.864.0", - "@aws-sdk/credential-provider-ini": "3.864.0", - "@aws-sdk/credential-provider-process": "3.864.0", - "@aws-sdk/credential-provider-sso": "3.864.0", - "@aws-sdk/credential-provider-web-identity": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.864.0.tgz", - "integrity": "sha512-Zxnn1hxhq7EOqXhVYgkF4rI9MnaO3+6bSg/tErnBQ3F8kDpA7CFU24G1YxwaJXp2X4aX3LwthefmSJHwcVP/2g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.864.0.tgz", - "integrity": "sha512-UPyPNQbxDwHVGmgWdGg9/9yvzuedRQVF5jtMkmP565YX9pKZ8wYAcXhcYdNPWFvH0GYdB0crKOmvib+bmCuwkw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.864.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/token-providers": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.864.0.tgz", - "integrity": "sha512-nNcjPN4SYg8drLwqK0vgVeSvxeGQiD0FxOaT38mV2H8cu0C5NzpvA+14Xy+W6vT84dxgmJYKk71Cr5QL2Oz+rA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/nested-clients": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.862.0.tgz", - "integrity": "sha512-jDje8dCFeFHfuCAxMDXBs8hy8q9NCTlyK4ThyyfAj3U4Pixly2mmzY2u7b7AyGhWsjJNx8uhTjlYq5zkQPQCYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.862.0.tgz", - "integrity": "sha512-N/bXSJznNBR/i7Ofmf9+gM6dx/SPBK09ZWLKsW5iQjqKxAKn/2DozlnE54uiEs1saHZWoNDRg69Ww4XYYSlG1Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.862.0.tgz", - "integrity": "sha512-KVoo3IOzEkTq97YKM4uxZcYFSNnMkhW/qj22csofLegZi5fk90ztUnnaeKfaEJHfHp/tm1Y3uSoOXH45s++kKQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.864.0.tgz", - "integrity": "sha512-GjYPZ6Xnqo17NnC8NIQyvvdzzO7dm+Ks7gpxD/HsbXPmV2aEfuFveJXneGW9e1BheSKFff6FPDWu8Gaj2Iu1yg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-arn-parser": "3.804.0", - "@smithy/core": "^3.8.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/protocol-http": "^5.1.3", - "@smithy/signature-v4": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-stream": "^4.2.4", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.864.0.tgz", - "integrity": "sha512-wrddonw4EyLNSNBrApzEhpSrDwJiNfjxDm5E+bn8n32BbAojXASH8W8jNpxz/jMgNkkJNxCfyqybGKzBX0OhbQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@smithy/core": "^3.8.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.864.0.tgz", - "integrity": "sha512-H1C+NjSmz2y8Tbgh7Yy89J20yD/hVyk15hNoZDbCYkXg0M358KS7KVIEYs8E2aPOCr1sK3HBE819D/yvdMgokA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.864.0", - "@aws-sdk/middleware-host-header": "3.862.0", - "@aws-sdk/middleware-logger": "3.862.0", - "@aws-sdk/middleware-recursion-detection": "3.862.0", - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/region-config-resolver": "3.862.0", - "@aws-sdk/types": "3.862.0", - "@aws-sdk/util-endpoints": "3.862.0", - "@aws-sdk/util-user-agent-browser": "3.862.0", - "@aws-sdk/util-user-agent-node": "3.864.0", - "@smithy/config-resolver": "^4.1.5", - "@smithy/core": "^3.8.0", - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/hash-node": "^4.0.5", - "@smithy/invalid-dependency": "^4.0.5", - "@smithy/middleware-content-length": "^4.0.5", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-retry": "^4.1.19", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/protocol-http": "^5.1.3", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.26", - "@smithy/util-defaults-mode-node": "^4.0.26", - "@smithy/util-endpoints": "^3.0.7", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.862.0.tgz", - "integrity": "sha512-VisR+/HuVFICrBPY+q9novEiE4b3mvDofWqyvmxHcWM7HumTz9ZQSuEtnlB/92GVM3KDUrR9EmBHNRrfXYZkcQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.864.0.tgz", - "integrity": "sha512-w2HIn/WIcUyv1bmyCpRUKHXB5KdFGzyxPkp/YK5g+/FuGdnFFYWGfcO8O+How4jwrZTarBYsAHW9ggoKvwr37w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/signature-v4": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.864.0.tgz", - "integrity": "sha512-gTc2QHOBo05SCwVA65dUtnJC6QERvFaPiuppGDSxoF7O5AQNK0UR/kMSenwLqN8b5E1oLYvQTv3C1idJLRX0cg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.864.0", - "@aws-sdk/nested-clients": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", - "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", - "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.862.0.tgz", - "integrity": "sha512-eCZuScdE9MWWkHGM2BJxm726MCmWk/dlHjOKvkM0sN1zxBellBMw5JohNss1Z8/TUmnW2gb9XHTOiHuGjOdksA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-endpoints": "^3.0.7", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", - "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.862.0.tgz", - "integrity": "sha512-BmPTlm0r9/10MMr5ND9E92r8KMZbq5ltYXYpVcUbAsnB1RJ8ASJuRoLne5F7mB3YMx0FJoOTuSq7LdQM3LgW3Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.862.0", - "@smithy/types": "^4.3.2", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.864.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.864.0.tgz", - "integrity": "sha512-d+FjUm2eJEpP+FRpVR3z6KzMdx1qwxEYDz8jzNKwxYLBBquaBaP/wfoMtMQKAcbrR7aT9FZVZF7zDgzNxUvQlQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.864.0", - "@aws-sdk/types": "3.862.0", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.862.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.862.0.tgz", - "integrity": "sha512-6Ed0kmC1NMbuFTEgNmamAUU1h5gShgxL1hBVLbEzUa3trX5aJBz1vU4bXaBTvOYUAnOHtiy1Ml4AMStd6hJnFA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.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, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", - "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/core/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, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "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, - "license": "MIT", - "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-compilation-targets/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, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/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, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "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, - "license": "MIT", - "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.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", - "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, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", - "license": "MIT", - "dependencies": { - "colorspace": "1.1.x", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.4", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", - "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", - "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", - "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", - "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", - "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", - "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", - "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.5.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", - "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.0.5", - "jest-util": "30.0.5", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", - "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.0.5", - "@jest/pattern": "30.0.1", - "@jest/reporters": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.0.5", - "jest-config": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-resolve-dependencies": "30.0.5", - "jest-runner": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "jest-watcher": "30.0.5", - "micromatch": "^4.0.8", - "pretty-format": "30.0.5", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", - "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-mock": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "30.0.5", - "jest-snapshot": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", - "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", - "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@sinonjs/fake-timers": "^13.0.0", - "@types/node": "*", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/get-type": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", - "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", - "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", - "@jest/types": "30.0.5", - "jest-mock": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", - "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.0.5", - "jest-util": "30.0.5", - "jest-worker": "30.0.5", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", - "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/test-result": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", - "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.0.5", - "@jest/types": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", - "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.0.5", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", - "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.0.5", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.0", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", - "micromatch": "^4.0.8", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/types": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", - "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "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==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@redis/bloom": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.1.tgz", - "integrity": "sha512-hJOJr/yX6BttnyZ+nxD3Ddiu2lPig4XJjyAK1v7OSHOJNUTfn3RHBryB9wgnBMBdkg9glVh2AjItxIXmr600MA==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "node_modules/@redis/client": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.1.tgz", - "integrity": "sha512-hD5Tvv7G0t8b3w8ao3kQ4jEPUmUUC6pqA18c8ciYF5xZGfUGBg0olQHW46v6qSt4O5bxOuB3uV7pM6H5wEjBwA==", - "license": "MIT", - "dependencies": { - "cluster-key-slot": "1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@redis/json": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.1.tgz", - "integrity": "sha512-kyvM8Vn+WjJI++nRsIoI9TbdfCs1/TgD0Hp7Z7GiG6W4IEBzkXGQakli+R5BoJzUfgh7gED2fkncYy1NLprMNg==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "node_modules/@redis/search": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.1.tgz", - "integrity": "sha512-CzuKNTInTNQkxqehSn7QiYcM+th+fhjQn5ilTvksP1wPjpxqK0qWt92oYg3XZc3tO2WuXkqDvTujc4D7kb6r/A==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "node_modules/@redis/time-series": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.1.tgz", - "integrity": "sha512-klvdR96U9oSOyqvcectoAGhYlMOnMS3I5UWUOgdBn1buMODiwM/E4Eds7gxldKmtowe4rLJSF1CyIqyZTjy8Ow==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@redis/client": "^5.8.1" - } - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.40", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz", - "integrity": "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@smithy/abort-controller": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", - "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", - "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.8.0.tgz", - "integrity": "sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/middleware-serde": "^4.0.9", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-stream": "^4.2.4", - "@smithy/util-utf8": "^4.0.0", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core/node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@smithy/core/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", - "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", - "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/querystring-builder": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", - "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", - "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", - "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.18.tgz", - "integrity": "sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.8.0", - "@smithy/middleware-serde": "^4.0.9", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "@smithy/url-parser": "^4.0.5", - "@smithy/util-middleware": "^4.0.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.1.19", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.19.tgz", - "integrity": "sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/protocol-http": "^5.1.3", - "@smithy/service-error-classification": "^4.0.7", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-retry": "^4.0.7", - "@types/uuid": "^9.0.1", - "tslib": "^2.6.2", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry/node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@smithy/middleware-retry/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", - "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", - "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", - "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.0.5", - "@smithy/shared-ini-file-loader": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", - "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/abort-controller": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/querystring-builder": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", - "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", - "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", - "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "@smithy/util-uri-escape": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", - "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", - "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", - "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", - "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.5", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.10.tgz", - "integrity": "sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.8.0", - "@smithy/middleware-endpoint": "^4.1.18", - "@smithy/middleware-stack": "^4.0.5", - "@smithy/protocol-http": "^5.1.3", - "@smithy/types": "^4.3.2", - "@smithy/util-stream": "^4.2.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", - "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", - "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.0.5", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", - "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", - "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", - "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", - "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.26.tgz", - "integrity": "sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.0.5", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.26.tgz", - "integrity": "sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.1.5", - "@smithy/credential-provider-imds": "^4.0.7", - "@smithy/node-config-provider": "^4.1.4", - "@smithy/property-provider": "^4.0.5", - "@smithy/smithy-client": "^4.4.10", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", - "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.1.4", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", - "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", - "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", - "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.0.7", - "@smithy/types": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", - "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/fetch-http-handler": "^5.1.1", - "@smithy/node-http-handler": "^4.1.1", - "@smithy/types": "^4.3.2", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", - "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/@sqltools/formatter": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", - "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/bcrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookie-parser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", - "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", - "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/multer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", - "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/node": { - "version": "24.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz", - "integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@types/nodemailer": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.1.tgz", - "integrity": "sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@aws-sdk/client-sesv2": "^3.839.0", - "@types/node": "*" - } - }, - "node_modules/@types/pg": { - "version": "8.15.5", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", - "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/redis": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.10.tgz", - "integrity": "sha512-7CLy5b5fzzEGVcOccgZjoMlNpPhX6d10jEeRy2YWbFuaMNrSPc9ExRsMYsd+0VxvEHucf4EWx24Ja7cSU1FGUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "redis": "*" - } - }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/socket.io": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", - "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "socket.io": "*" - } - }, - "node_modules/@types/socket.io-client": { - "version": "1.4.36", - "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", - "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/superagent": { - "version": "8.1.9", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", - "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/supertest": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", - "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" - } - }, - "node_modules/@types/swagger-jsdoc": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", - "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/swagger-ui-express": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", - "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@zxing/text-encoding": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", - "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "license": "(Unlicense OR Apache-2.0)", - "optional": true - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "devOptional": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansis": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", - "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", - "license": "ISC", - "engines": { - "node": ">=14" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/app-root-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", - "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true, - "license": "MIT" - }, - "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==", - "license": "Python-2.0" - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/babel-jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", - "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "30.0.5", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.0", - "babel-preset-jest": "30.0.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", - "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", - "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "@types/babel__core": "^7.20.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", - "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.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==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/bcrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/block-stream2": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", - "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", - "license": "MIT", - "dependencies": { - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/bowser": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz", - "integrity": "sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==", - "dev": true, - "license": "MIT" - }, - "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==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "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, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-or-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", - "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", - "license": "MIT" - }, - "node_modules/browserslist": { - "version": "4.25.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz", - "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001735", - "electron-to-chromium": "^1.5.204", - "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/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "license": "MIT" - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001735", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", - "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", - "dev": true, - "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" - } - ], - "license": "CC-BY-4.0" - }, - "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, - "license": "MIT", - "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/chalk/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, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/chalk/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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/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, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/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==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "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, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dezalgo": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", - "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.207", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz", - "integrity": "sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "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==", - "dev": true, - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/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==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "dev": true, - "license": "MIT", - "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/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "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, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "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, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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==", - "dev": true, - "license": "MIT", - "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/form-data/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/formidable": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", - "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@paralleldrive/cuid2": "^2.2.2", - "dezalgo": "^1.0.4", - "once": "^1.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "url": "https://ko-fi.com/tunnckoCore/commissions" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "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, - "license": "MIT", - "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==", - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "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==", - "license": "MIT", - "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-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "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==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/helmet": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", - "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "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, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/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, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", - "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.0.5", - "@jest/types": "30.0.5", - "import-local": "^3.2.0", - "jest-cli": "30.0.5" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", - "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.0.5", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", - "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", - "jest-util": "30.0.5", - "p-limit": "^3.1.0", - "pretty-format": "30.0.5", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-cli": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", - "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/types": "30.0.5", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", - "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.0.1", - "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.0.5", - "@jest/types": "30.0.5", - "babel-jest": "30.0.5", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-circus": "30.0.5", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-runner": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "micromatch": "^4.0.8", - "parse-json": "^5.2.0", - "pretty-format": "30.0.5", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", - "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", - "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", - "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "@jest/types": "30.0.5", - "chalk": "^4.1.2", - "jest-util": "30.0.5", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", - "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", - "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", - "jest-worker": "30.0.5", - "micromatch": "^4.0.8", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-leak-detector": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", - "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", - "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "chalk": "^4.1.2", - "jest-diff": "30.0.5", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", - "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.0.5", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "jest-util": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", - "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", - "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "30.0.1", - "jest-snapshot": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", - "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.0.5", - "@jest/environment": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-leak-detector": "30.0.5", - "jest-message-util": "30.0.5", - "jest-resolve": "30.0.5", - "jest-runtime": "30.0.5", - "jest-util": "30.0.5", - "jest-watcher": "30.0.5", - "jest-worker": "30.0.5", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", - "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/globals": "30.0.5", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.3.10", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", - "jest-mock": "30.0.5", - "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-snapshot": "30.0.5", - "jest-util": "30.0.5", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime/node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-snapshot": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", - "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.5", - "@jest/transform": "30.0.5", - "@jest/types": "30.0.5", - "babel-preset-current-node-syntax": "^1.1.0", - "chalk": "^4.1.2", - "expect": "30.0.5", - "graceful-fs": "^4.2.11", - "jest-diff": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-util": "30.0.5", - "pretty-format": "30.0.5", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.0.5", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/jest-validate": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", - "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.0.1", - "@jest/types": "30.0.5", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.0.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", - "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.0.5", - "@jest/types": "30.0.5", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.0.5", - "string-length": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", - "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.5", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker/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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "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, - "license": "MIT" - }, - "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==", - "license": "MIT", - "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, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "license": "MIT" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "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, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minio": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.5.tgz", - "integrity": "sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.4", - "block-stream2": "^2.1.0", - "browser-or-node": "^2.1.1", - "buffer-crc32": "^1.0.0", - "eventemitter3": "^5.0.1", - "fast-xml-parser": "^4.4.1", - "ipaddr.js": "^2.0.1", - "lodash": "^4.17.21", - "mime-types": "^2.1.35", - "query-string": "^7.1.3", - "stream-json": "^1.8.0", - "through2": "^4.0.2", - "web-encoding": "^1.1.5", - "xml2js": "^0.5.0 || ^0.6.2" - }, - "engines": { - "node": "^16 || ^18 || >=20" - } - }, - "node_modules/minio/node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.1.1" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/minio/node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/minio/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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minio/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==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minio/node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" - }, - "engines": { - "node": ">= 10.16.0" - } - }, - "node_modules/multer/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/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==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "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, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/nodemailer": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", - "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/nodemon": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", - "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/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, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nodemon/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, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "license": "MIT", - "peer": true - }, - "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, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.2.7" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redis": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/redis/-/redis-5.8.1.tgz", - "integrity": "sha512-RZjBKYX/qFF809x6vDcE5VA6L3MmiuT+BkbXbIyyyeU0lPD47V4z8qTzN+Z/kKFwpojwCItOfaItYuAjNs8pTQ==", - "license": "MIT", - "dependencies": { - "@redis/bloom": "5.8.1", - "@redis/client": "5.8.1", - "@redis/json": "5.8.1", - "@redis/search": "5.8.1", - "@redis/time-series": "5.8.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, - "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==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sha.js": { - "version": "2.4.12", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.0" - }, - "bin": { - "sha.js": "bin.js" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sharp": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", - "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.4", - "@img/sharp-darwin-x64": "0.34.4", - "@img/sharp-libvips-darwin-arm64": "1.2.3", - "@img/sharp-libvips-darwin-x64": "1.2.3", - "@img/sharp-libvips-linux-arm": "1.2.3", - "@img/sharp-libvips-linux-arm64": "1.2.3", - "@img/sharp-libvips-linux-ppc64": "1.2.3", - "@img/sharp-libvips-linux-s390x": "1.2.3", - "@img/sharp-libvips-linux-x64": "1.2.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", - "@img/sharp-libvips-linuxmusl-x64": "1.2.3", - "@img/sharp-linux-arm": "0.34.4", - "@img/sharp-linux-arm64": "0.34.4", - "@img/sharp-linux-ppc64": "0.34.4", - "@img/sharp-linux-s390x": "0.34.4", - "@img/sharp-linux-x64": "0.34.4", - "@img/sharp-linuxmusl-arm64": "0.34.4", - "@img/sharp-linuxmusl-x64": "0.34.4", - "@img/sharp-wasm32": "0.34.4", - "@img/sharp-win32-arm64": "0.34.4", - "@img/sharp-win32-ia32": "0.34.4", - "@img/sharp-win32-x64": "0.34.4" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "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==", - "dev": true, - "license": "MIT", - "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==", - "dev": true, - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/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==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/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==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/socket.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/sql-highlight": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", - "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", - "funding": [ - "https://github.com/scriptcoded/sql-highlight?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/scriptcoded" - } - ], - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-chain": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", - "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", - "license": "BSD-3-Clause" - }, - "node_modules/stream-json": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", - "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", - "license": "BSD-3-Clause", - "dependencies": { - "stream-chain": "^2.2.5" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/superagent": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", - "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "component-emitter": "^1.3.1", - "cookiejar": "^2.1.4", - "debug": "^4.3.7", - "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.4", - "formidable": "^3.5.4", - "methods": "^1.1.2", - "mime": "2.6.0", - "qs": "^6.11.2" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supertest": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", - "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "methods": "^1.1.2", - "superagent": "^10.2.3" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", - "license": "MIT", - "dependencies": { - "commander": "6.2.0", - "doctrine": "3.0.0", - "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", - "yaml": "2.0.0-1" - }, - "bin": { - "swagger-jsdoc": "bin/swagger-jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/swagger-jsdoc/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==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/swagger-jsdoc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.27.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.1.tgz", - "integrity": "sha512-oGtpYO3lnoaqyGtlJalvryl7TwzgRuxpOVWqEHx8af0YXI+Kt+4jMpLdgMtMcmWmuQ0QTCHLKExwrBFMSxvAUA==", - "license": "Apache-2.0", - "dependencies": { - "@scarf/scarf": "=1.4.0" - } - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "license": "MIT", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, - "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/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, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/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, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "license": "MIT", - "dependencies": { - "readable-stream": "3" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", - "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "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, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ts-jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", - "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.7.2", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "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==", - "license": "0BSD" - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, - "node_modules/typeorm": { - "version": "0.3.26", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", - "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==", - "license": "MIT", - "dependencies": { - "@sqltools/formatter": "^1.2.5", - "ansis": "^3.17.0", - "app-root-path": "^3.1.0", - "buffer": "^6.0.3", - "dayjs": "^1.11.13", - "debug": "^4.4.0", - "dedent": "^1.6.0", - "dotenv": "^16.4.7", - "glob": "^10.4.5", - "sha.js": "^2.4.11", - "sql-highlight": "^6.0.0", - "tslib": "^2.8.1", - "uuid": "^11.1.0", - "yargs": "^17.7.2" - }, - "bin": { - "typeorm": "cli.js", - "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", - "typeorm-ts-node-esm": "cli-ts-node-esm.js" - }, - "engines": { - "node": ">=16.13.0" - }, - "funding": { - "url": "https://opencollective.com/typeorm" - }, - "peerDependencies": { - "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", - "@sap/hana-client": "^2.14.22", - "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", - "ioredis": "^5.0.4", - "mongodb": "^5.8.0 || ^6.0.0", - "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", - "mysql2": "^2.2.5 || ^3.0.1", - "oracledb": "^6.3.0", - "pg": "^8.5.1", - "pg-native": "^3.0.0", - "pg-query-stream": "^4.0.0", - "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", - "reflect-metadata": "^0.1.14 || ^0.2.0", - "sql.js": "^1.4.0", - "sqlite3": "^5.0.3", - "ts-node": "^10.7.0", - "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" - }, - "peerDependenciesMeta": { - "@google-cloud/spanner": { - "optional": true - }, - "@sap/hana-client": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "mssql": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "oracledb": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-native": { - "optional": true - }, - "pg-query-stream": { - "optional": true - }, - "redis": { - "optional": true - }, - "sql.js": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "ts-node": { - "optional": true - }, - "typeorm-aurora-data-api-driver": { - "optional": true - } - } - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, - "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==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/web-encoding": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", - "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", - "license": "MIT", - "dependencies": { - "util": "^0.12.3" - }, - "optionalDependencies": { - "@zxing/text-encoding": "0.9.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/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==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.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==", - "license": "MIT", - "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/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.0.0-1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", - "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "license": "MIT", - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" - } - } - } -} diff --git a/SerpentRace_Backend/package.json b/SerpentRace_Backend/package.json deleted file mode 100644 index 0999f1bd..00000000 --- a/SerpentRace_Backend/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "name": "serpentrace_backend", - "version": "1.0.0", - "description": "", - "license": "ISC", - "author": "", - "type": "commonjs", - "main": "index.js", - "scripts": { - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "test:redis": "jest --testNamePattern=\"RedisService\"", - "start": "node ./dist/Api/index.js", - "dev": "nodemon --watch src --ext ts,json --exec ts-node ./src/Api/index.ts", - "build": "npm run build:clean && npm run build:compile && npm run build:copy-assets", - "build:clean": "rimraf dist", - "build:compile": "tsc", - "build:copy-assets": "node scripts/copy-assets.js", - "build:production": "npm run build:clean && npm run lint && npm run test && npm run migration:run && npm run build:compile && npm run build:copy-assets", - "build:docker": "npm run build:clean && npm run build:compile && npm run build:copy-assets", - "build:advanced": "ts-node scripts/build.ts", - "build:advanced:prod": "ts-node scripts/build.ts --production --migrations --test", - "build:advanced:ci": "ts-node scripts/build.ts --production --migrations --test --skip-lint", - "deploy": "node -e \"console.log('Use deploy.bat on Windows or deploy.sh on Linux/Mac')\"", - "deploy:prod": "npm run build:production && echo 'Build completed - ready for deployment'", - "build:help": "node scripts/build-help.js", - "build:status": "node scripts/build-help.js --status", - "build:quick": "node scripts/build-help.js --quick", - "prebuild": "npm run lint", - "postbuild": "echo 'Build completed successfully!'", - "lint": "echo 'Linting...' && echo 'No linter configured - add ESLint if needed'", - "migration:create": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli migration:create", - "migration:generate": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:generate", - "migration:run": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:run", - "migration:revert": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:revert", - "migration:show": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:show", - "migration:full": "ts-node scripts/generate-migration.ts", - "typecheck": "tsc --noEmit", - "watch": "tsc --watch" - }, - "dependencies": { - "bcrypt": "^6.0.0", - "cookie-parser": "^1.4.7", - "express": "^5.1.0", - "helmet": "^8.1.0", - "jsonwebtoken": "^9.0.2", - "minio": "^8.0.5", - "multer": "^2.0.2", - "nodemailer": "^7.0.5", - "pg": "^8.16.3", - "redis": "^5.8.1", - "sharp": "^0.34.4", - "socket.io": "^4.8.1", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", - "tsconfig-paths": "^4.2.0", - "typeorm": "^0.3.26", - "uuid": "^11.1.0", - "winston": "^3.17.0" - }, - "devDependencies": { - "@jest/globals": "^30.0.5", - "@types/bcrypt": "^6.0.0", - "@types/cookie-parser": "^1.4.9", - "@types/express": "^5.0.3", - "@types/jest": "^30.0.0", - "@types/jsonwebtoken": "^9.0.10", - "@types/multer": "^2.0.0", - "@types/node": "^24.3.3", - "@types/nodemailer": "^7.0.1", - "@types/pg": "^8.15.5", - "@types/redis": "^4.0.10", - "@types/socket.io": "^3.0.1", - "@types/socket.io-client": "^1.4.36", - "@types/supertest": "^6.0.3", - "@types/swagger-jsdoc": "^6.0.4", - "@types/swagger-ui-express": "^4.1.8", - "@types/uuid": "^10.0.0", - "jest": "^30.0.5", - "nodemon": "^3.1.10", - "rimraf": "^5.0.10", - "socket.io-client": "^4.8.1", - "supertest": "^7.1.4", - "ts-jest": "^29.4.1", - "ts-node": "^10.9.2", - "typescript": "^5.9.2" - } -} diff --git a/SerpentRace_Backend/scripts/build-help.js b/SerpentRace_Backend/scripts/build-help.js deleted file mode 100644 index 183e9dff..00000000 --- a/SerpentRace_Backend/scripts/build-help.js +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env node - -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -/** - * Build System Helper - Shows available build commands and their descriptions - */ - -const commands = { - 'Development Commands': { - 'npm run dev': 'Start development server with hot reload', - 'npm run watch': 'Watch mode TypeScript compilation', - 'npm run typecheck': 'Type checking without code generation' - }, - 'Build Commands': { - 'npm run build': 'Standard build: clean → compile → copy assets', - 'npm run build:clean': 'Clean the dist directory', - 'npm run build:compile': 'Compile TypeScript to JavaScript', - 'npm run build:copy-assets': 'Copy non-TS files to dist directory', - 'npm run build:docker': 'Build for Docker (no tests/migrations)' - }, - 'Production Build Commands': { - 'npm run build:production': 'Full production build with linting, tests, and migrations', - 'npm run build:advanced': 'Advanced build script with custom options', - 'npm run build:advanced:prod': 'Advanced production build with all validations', - 'npm run build:advanced:ci': 'CI/CD friendly build (skips linting)', - 'npm run deploy:prod': 'Build for production deployment' - }, - 'Database Commands': { - 'npm run migration:run': 'Run pending database migrations', - 'npm run migration:show': 'Show migration status', - 'npm run migration:generate ': 'Generate new migration', - 'npm run migration:create ': 'Create empty migration', - 'npm run migration:revert': 'Revert last migration', - 'npm run migration:full ': 'Create, generate, and run migration' - }, - 'Testing Commands': { - 'npm test': 'Run all tests', - 'npm run test:watch': 'Run tests in watch mode', - 'npm run test:coverage': 'Run tests with coverage report', - 'npm run test:redis': 'Run Redis-specific tests' - }, - 'Deployment Scripts': { - 'scripts/deploy.sh': 'Full Linux/Mac deployment script', - 'scripts/deploy.bat': 'Full Windows deployment script' - } -}; - -function showCommands() { - console.log('🔧 SerpentRace Backend Build System\n'); - - Object.entries(commands).forEach(([category, categoryCommands]) => { - console.log(`\x1b[36m${category}\x1b[0m`); - console.log('=' .repeat(category.length)); - - Object.entries(categoryCommands).forEach(([command, description]) => { - console.log(` \x1b[32m${command.padEnd(35)}\x1b[0m ${description}`); - }); - - console.log(''); - }); - - console.log('\x1b[33mQuick Start:\x1b[0m'); - console.log(' npm run build # Basic build'); - console.log(' npm run build:production # Production build'); - console.log(' npm run dev # Development server\n'); - - console.log('\x1b[33mDocumentation:\x1b[0m'); - console.log(' See BUILD.md for detailed documentation'); -} - -function checkBuildStatus() { - const distPath = path.join(__dirname, '..', 'dist'); - - if (fs.existsSync(distPath)) { - const stats = fs.statSync(distPath); - console.log(`\x1b[32m✅ Last build:\x1b[0m ${stats.mtime.toLocaleString()}`); - - const indexPath = path.join(distPath, 'Api', 'index.js'); - if (fs.existsSync(indexPath)) { - console.log('\x1b[32m✅ Main entry point built successfully\x1b[0m'); - } else { - console.log('\x1b[31m❌ Main entry point missing\x1b[0m'); - } - } else { - console.log('\x1b[33m⚠️ No build found - run "npm run build" first\x1b[0m'); - } -} - -// Handle command line arguments -const args = process.argv.slice(2); - -if (args.includes('--help') || args.includes('-h')) { - showCommands(); -} else if (args.includes('--status') || args.includes('-s')) { - checkBuildStatus(); -} else if (args.includes('--quick') || args.includes('-q')) { - console.log('🚀 Quick build starting...'); - try { - execSync('npm run build', { stdio: 'inherit' }); - } catch (error) { - console.error('❌ Quick build failed'); - process.exit(1); - } -} else { - showCommands(); - checkBuildStatus(); - - console.log('\n\x1b[33mOptions:\x1b[0m'); - console.log(' --help, -h Show this help'); - console.log(' --status, -s Show build status only'); - console.log(' --quick, -q Run quick build'); -} diff --git a/SerpentRace_Backend/scripts/build.ts b/SerpentRace_Backend/scripts/build.ts deleted file mode 100644 index 81185407..00000000 --- a/SerpentRace_Backend/scripts/build.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { execSync } from 'child_process'; -import { existsSync, rmSync } from 'fs'; -import { join } from 'path'; - -/** - * Comprehensive Build Script for SerpentRace Backend - * Handles TypeScript compilation, migrations, asset copying, and validation - */ - -interface BuildOptions { - runMigrations?: boolean; - runTests?: boolean; - skipLinting?: boolean; - production?: boolean; -} - -class BuildManager { - private distDir = join(__dirname, '..', 'dist'); - - constructor(private options: BuildOptions = {}) {} - - private log(message: string, level: 'info' | 'error' | 'warn' = 'info') { - const timestamp = new Date().toISOString(); - const prefix = { - info: '🔧', - error: '❌', - warn: '⚠️' - }[level]; - console.log(`${prefix} [${timestamp}] ${message}`); - } - - private execute(command: string, description: string) { - this.log(`${description}...`); - try { - execSync(command, { - stdio: 'inherit', - cwd: join(__dirname, '..') - }); - this.log(`✅ ${description} completed successfully`); - } catch (error) { - this.log(`❌ ${description} failed`, 'error'); - throw error; - } - } - - async clean() { - this.log('Cleaning previous build...'); - if (existsSync(this.distDir)) { - rmSync(this.distDir, { recursive: true, force: true }); - this.log('✅ Previous build cleaned'); - } else { - this.log('No previous build found'); - } - } - - async typecheck() { - this.execute('npx tsc --noEmit', 'Type checking'); - } - - async lint() { - if (this.options.skipLinting) { - this.log('Skipping linting...', 'warn'); - return; - } - - // For now, just check if TypeScript compiles without errors - this.log('Linting (basic type checking)...'); - await this.typecheck(); - } - - async runTests() { - if (!this.options.runTests) { - this.log('Skipping tests...', 'warn'); - return; - } - - this.execute('npm test', 'Running tests'); - } - - async runMigrations() { - if (!this.options.runMigrations) { - this.log('Skipping database migrations...', 'warn'); - return; - } - - try { - this.log('Checking migration status...'); - execSync('npm run migration:show', { - stdio: 'pipe', - cwd: join(__dirname, '..') - }); - - this.execute('npm run migration:run', 'Running database migrations'); - } catch (error) { - this.log('Migration check/run failed - this might be expected in CI/CD environments', 'warn'); - if (this.options.production) { - throw error; // In production builds, migrations should work - } - } - } - - async compile() { - this.execute('npx tsc', 'Compiling TypeScript'); - } - - async copyAssets() { - this.execute('node scripts/copy-assets.js', 'Copying assets'); - } - - async validateBuild() { - this.log('Validating build output...'); - - const expectedFiles = [ - 'dist/Api/index.js', - 'dist/Api/index.d.ts' - ]; - - const missingFiles = expectedFiles.filter(file => - !existsSync(join(__dirname, '..', file)) - ); - - if (missingFiles.length > 0) { - this.log(`Missing expected build files: ${missingFiles.join(', ')}`, 'error'); - throw new Error('Build validation failed'); - } - - this.log('✅ Build validation completed'); - } - - async build() { - const startTime = Date.now(); - - try { - this.log('🚀 Starting SerpentRace Backend build process...'); - - // Step 1: Clean previous build - await this.clean(); - - // Step 2: Lint code (if not skipped) - await this.lint(); - - // Step 3: Run tests (if enabled) - await this.runTests(); - - // Step 4: Run migrations (if enabled) - await this.runMigrations(); - - // Step 5: Compile TypeScript - await this.compile(); - - // Step 6: Copy assets - await this.copyAssets(); - - // Step 7: Validate build - await this.validateBuild(); - - const duration = ((Date.now() - startTime) / 1000).toFixed(2); - this.log(`🎉 Build completed successfully in ${duration}s`); - - } catch (error) { - const duration = ((Date.now() - startTime) / 1000).toFixed(2); - this.log(`💥 Build failed after ${duration}s`, 'error'); - - if (error instanceof Error) { - this.log(`Error: ${error.message}`, 'error'); - } - - process.exit(1); - } - } -} - -// Parse command line arguments -const args = process.argv.slice(2); -const options: BuildOptions = { - runMigrations: args.includes('--migrations'), - runTests: args.includes('--test'), - skipLinting: args.includes('--skip-lint'), - production: args.includes('--production') -}; - -// Create and run build -const buildManager = new BuildManager(options); -buildManager.build().catch(error => { - console.error('Unhandled build error:', error); - process.exit(1); -}); diff --git a/SerpentRace_Backend/scripts/copy-assets.js b/SerpentRace_Backend/scripts/copy-assets.js deleted file mode 100644 index 1f9c26e3..00000000 --- a/SerpentRace_Backend/scripts/copy-assets.js +++ /dev/null @@ -1,62 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -/** - * Copy Assets Script for SerpentRace Backend - * Copies non-TypeScript files to the dist directory - */ - -const srcDir = path.join(__dirname, '..', 'src'); -const distDir = path.join(__dirname, '..', 'dist'); - -// File extensions to copy -const assetExtensions = ['.json', '.html', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot']; - -// Directories to exclude from copying -const excludeDirs = ['node_modules', '.git', 'tests', '__tests__']; - -function copyAssets(srcPath, distPath) { - if (!fs.existsSync(srcPath)) { - console.log(`Source directory ${srcPath} does not exist`); - return; - } - - if (!fs.existsSync(distPath)) { - fs.mkdirSync(distPath, { recursive: true }); - } - - const items = fs.readdirSync(srcPath); - - items.forEach(item => { - const srcItemPath = path.join(srcPath, item); - const distItemPath = path.join(distPath, item); - const stat = fs.statSync(srcItemPath); - - if (stat.isDirectory()) { - // Skip excluded directories - if (excludeDirs.includes(item)) { - return; - } - - // Recursively copy subdirectories - copyAssets(srcItemPath, distItemPath); - } else { - const ext = path.extname(item).toLowerCase(); - - // Copy asset files - if (assetExtensions.includes(ext)) { - console.log(`Copying asset: ${srcItemPath} -> ${distItemPath}`); - fs.copyFileSync(srcItemPath, distItemPath); - } - } - }); -} - -try { - console.log('Copying assets from src to dist...'); - copyAssets(srcDir, distDir); - console.log('Asset copying completed successfully!'); -} catch (error) { - console.error('Error copying assets:', error); - process.exit(1); -} diff --git a/SerpentRace_Backend/scripts/deploy.bat b/SerpentRace_Backend/scripts/deploy.bat deleted file mode 100644 index 465ad1dc..00000000 --- a/SerpentRace_Backend/scripts/deploy.bat +++ /dev/null @@ -1,233 +0,0 @@ -@echo off -REM SerpentRace Backend Production Deployment Script for Windows -REM This script handles the complete deployment process - -setlocal EnableDelayedExpansion - -set "SCRIPT_START=%TIME%" - -REM Colors simulation for Windows (using echo with different prefixes) -set "LOG_PREFIX=[INFO]" -set "ERROR_PREFIX=[ERROR]" -set "WARN_PREFIX=[WARN]" - -:log -echo %LOG_PREFIX% [%DATE% %TIME%] %~1 -goto :eof - -:error -echo %ERROR_PREFIX% [%DATE% %TIME%] %~1 -goto :eof - -:warn -echo %WARN_PREFIX% [%DATE% %TIME%] %~1 -goto :eof - -:check_env -call :log "Checking environment variables..." - -set "required_vars=DB_HOST DB_PORT DB_USERNAME DB_PASSWORD DB_NAME JWT_SECRET REDIS_HOST REDIS_PORT" -set "missing_vars=" - -for %%v in (%required_vars%) do ( - call set "var_value=%%!%%v!%%" - if "!var_value!"=="" ( - set "missing_vars=!missing_vars! %%v" - ) -) - -if not "!missing_vars!"==" " ( - call :error "Missing required environment variables:!missing_vars!" - call :error "Please set these variables before running the deployment" - exit /b 1 -) - -call :log "All required environment variables are set" -goto :eof - -:install_dependencies -call :log "Installing production dependencies..." -npm ci --only=production -if !errorlevel! neq 0 ( - call :error "Failed to install dependencies" - exit /b 1 -) -call :log "Dependencies installed successfully" -goto :eof - -:run_build -call :log "Running production build..." -npm run build:production -if !errorlevel! neq 0 ( - call :error "Build failed" - exit /b 1 -) -call :log "Build completed successfully" -goto :eof - -:test_database -call :log "Testing database connectivity..." - -echo import { AppDataSource } from './src/Infrastructure/ormconfig'; > test-db-temp.ts -echo. >> test-db-temp.ts -echo async function testConnection() { >> test-db-temp.ts -echo try { >> test-db-temp.ts -echo await AppDataSource.initialize(); >> test-db-temp.ts -echo console.log('✅ Database connection successful'^); >> test-db-temp.ts -echo await AppDataSource.destroy(); >> test-db-temp.ts -echo process.exit(0^); >> test-db-temp.ts -echo } catch (error^) { >> test-db-temp.ts -echo console.error('❌ Database connection failed:', error^); >> test-db-temp.ts -echo process.exit(1^); >> test-db-temp.ts -echo } >> test-db-temp.ts -echo } >> test-db-temp.ts -echo. >> test-db-temp.ts -echo testConnection(); >> test-db-temp.ts - -npx ts-node test-db-temp.ts -set "db_test_result=!errorlevel!" -del test-db-temp.ts 2>nul - -if !db_test_result! neq 0 ( - call :error "Database connectivity test failed" - exit /b 1 -) - -call :log "Database connectivity test passed" -goto :eof - -:test_redis -call :log "Testing Redis connectivity..." - -echo import { createClient } from 'redis'; > test-redis-temp.ts -echo. >> test-redis-temp.ts -echo async function testRedis() { >> test-redis-temp.ts -echo const client = createClient({ >> test-redis-temp.ts -echo socket: { >> test-redis-temp.ts -echo host: process.env.REDIS_HOST ^|^| 'localhost', >> test-redis-temp.ts -echo port: parseInt(process.env.REDIS_PORT ^|^| '6379'^) >> test-redis-temp.ts -echo } >> test-redis-temp.ts -echo }^); >> test-redis-temp.ts -echo. >> test-redis-temp.ts -echo try { >> test-redis-temp.ts -echo await client.connect(); >> test-redis-temp.ts -echo await client.ping(); >> test-redis-temp.ts -echo console.log('✅ Redis connection successful'^); >> test-redis-temp.ts -echo await client.disconnect(); >> test-redis-temp.ts -echo process.exit(0^); >> test-redis-temp.ts -echo } catch (error^) { >> test-redis-temp.ts -echo console.error('❌ Redis connection failed:', error^); >> test-redis-temp.ts -echo process.exit(1^); >> test-redis-temp.ts -echo } >> test-redis-temp.ts -echo } >> test-redis-temp.ts -echo. >> test-redis-temp.ts -echo testRedis(); >> test-redis-temp.ts - -npx ts-node test-redis-temp.ts -set "redis_test_result=!errorlevel!" -del test-redis-temp.ts 2>nul - -if !redis_test_result! neq 0 ( - call :warn "Redis connectivity test failed - continuing anyway" -) else ( - call :log "Redis connectivity test passed" -) -goto :eof - -:setup_directories -call :log "Setting up required directories..." -if not exist "logs" mkdir logs -if not exist "uploads" mkdir uploads -call :log "Directories created" -goto :eof - -:start_app -call :log "Starting application for validation..." - -REM Start the app in background -start /B "" npm start - -REM Wait for app to start -timeout /t 10 /nobreak >nul - -REM Test if the health endpoint responds (using curl if available) -set "PORT_VAR=!PORT!" -if "!PORT_VAR!"=="" set "PORT_VAR=3000" - -curl -f http://localhost:!PORT_VAR!/health >nul 2>&1 -if !errorlevel! equ 0 ( - call :log "Application health check passed" - REM Try to stop the background process (this is tricky in batch) - taskkill /F /IM node.exe /FI "WINDOWTITLE eq npm start*" >nul 2>&1 -) else ( - call :error "Application health check failed" - taskkill /F /IM node.exe /FI "WINDOWTITLE eq npm start*" >nul 2>&1 - exit /b 1 -) -goto :eof - -:deploy -call :log "🚀 Starting SerpentRace Backend production deployment..." - -call :check_env -if !errorlevel! neq 0 exit /b 1 - -call :install_dependencies -if !errorlevel! neq 0 exit /b 1 - -call :run_build -if !errorlevel! neq 0 exit /b 1 - -call :setup_directories -if !errorlevel! neq 0 exit /b 1 - -call :test_database -if !errorlevel! neq 0 exit /b 1 - -call :test_redis -REM Redis test failure is not fatal - -if not "%SKIP_APP_TEST%"=="true" ( - call :start_app - if !errorlevel! neq 0 exit /b 1 -) else ( - call :warn "Skipping application startup test" -) - -call :log "🎉 Deployment completed successfully!" -call :log "You can now start the application with: npm start" -goto :eof - -:build_only -call :log "Running build-only deployment..." -call :check_env -if !errorlevel! neq 0 exit /b 1 -call :install_dependencies -if !errorlevel! neq 0 exit /b 1 -call :run_build -if !errorlevel! neq 0 exit /b 1 -call :setup_directories -call :log "Build-only deployment completed" -goto :eof - -:test_connections -call :log "Testing connections only..." -call :check_env -if !errorlevel! neq 0 exit /b 1 -call :test_database -if !errorlevel! neq 0 exit /b 1 -call :test_redis -call :log "Connection tests completed" -goto :eof - -REM Main script logic -if "%1"=="" goto deploy -if "%1"=="deploy" goto deploy -if "%1"=="build-only" goto build_only -if "%1"=="test-connections" goto test_connections - -echo Usage: %0 [deploy^|build-only^|test-connections] -echo deploy - Full deployment (default) -echo build-only - Only build, skip tests -echo test-connections - Test database and Redis connections -exit /b 1 diff --git a/SerpentRace_Backend/scripts/deploy.sh b/SerpentRace_Backend/scripts/deploy.sh deleted file mode 100644 index 4af6b641..00000000 --- a/SerpentRace_Backend/scripts/deploy.sh +++ /dev/null @@ -1,237 +0,0 @@ -#!/bin/bash - -# SerpentRace Backend Production Deployment Script -# This script handles the complete deployment process - -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' -NC='\033[0m' # No Color - -log() { - echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" -} - -error() { - echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" -} - -warn() { - echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" -} - -info() { - echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $1${NC}" -} - -# Check if required environment variables are set -check_env() { - log "Checking environment variables..." - - required_vars=( - "DB_HOST" - "DB_PORT" - "DB_USERNAME" - "DB_PASSWORD" - "DB_NAME" - "JWT_SECRET" - "REDIS_HOST" - "REDIS_PORT" - ) - - missing_vars=() - for var in "${required_vars[@]}"; do - if [ -z "${!var}" ]; then - missing_vars+=("$var") - fi - done - - if [ ${#missing_vars[@]} -ne 0 ]; then - error "Missing required environment variables: ${missing_vars[*]}" - error "Please set these variables before running the deployment" - exit 1 - fi - - log "All required environment variables are set" -} - -# Install dependencies -install_dependencies() { - log "Installing production dependencies..." - npm ci --only=production - log "Dependencies installed successfully" -} - -# Run the comprehensive build process -run_build() { - log "Running production build..." - npm run build:production - log "Build completed successfully" -} - -# Test database connectivity -test_database() { - log "Testing database connectivity..." - - # Use a simple TypeScript script to test connection - cat > /tmp/test-db.ts << 'EOF' -import { AppDataSource } from './src/Infrastructure/ormconfig'; - -async function testConnection() { - try { - await AppDataSource.initialize(); - console.log('✅ Database connection successful'); - await AppDataSource.destroy(); - process.exit(0); - } catch (error) { - console.error('❌ Database connection failed:', error); - process.exit(1); - } -} - -testConnection(); -EOF - - npx ts-node /tmp/test-db.ts || { - error "Database connectivity test failed" - exit 1 - } - - rm -f /tmp/test-db.ts - log "Database connectivity test passed" -} - -# Test Redis connectivity -test_redis() { - log "Testing Redis connectivity..." - - # Use a simple script to test Redis connection - cat > /tmp/test-redis.ts << 'EOF' -import { createClient } from 'redis'; - -async function testRedis() { - const client = createClient({ - socket: { - host: process.env.REDIS_HOST || 'localhost', - port: parseInt(process.env.REDIS_PORT || '6379') - } - }); - - try { - await client.connect(); - await client.ping(); - console.log('✅ Redis connection successful'); - await client.disconnect(); - process.exit(0); - } catch (error) { - console.error('❌ Redis connection failed:', error); - process.exit(1); - } -} - -testRedis(); -EOF - - npx ts-node /tmp/test-redis.ts || { - warn "Redis connectivity test failed - continuing anyway" - } - - rm -f /tmp/test-redis.ts - log "Redis connectivity test completed" -} - -# Create required directories -setup_directories() { - log "Setting up required directories..." - mkdir -p logs - mkdir -p uploads - log "Directories created" -} - -# Start the application (for testing) -start_app() { - log "Starting application for validation..." - - # Start the app in background and test if it responds - npm start & - APP_PID=$! - - # Wait for app to start - sleep 10 - - # Test if the health endpoint responds - if curl -f http://localhost:${PORT:-3000}/health > /dev/null 2>&1; then - log "Application health check passed" - kill $APP_PID - wait $APP_PID 2>/dev/null - else - error "Application health check failed" - kill $APP_PID 2>/dev/null || true - wait $APP_PID 2>/dev/null || true - exit 1 - fi -} - -# Main deployment function -deploy() { - log "🚀 Starting SerpentRace Backend production deployment..." - - # Check environment - check_env - - # Install dependencies - install_dependencies - - # Run build process - run_build - - # Setup directories - setup_directories - - # Test connections - test_database - test_redis - - # Test application startup - if [ "${SKIP_APP_TEST}" != "true" ]; then - start_app - else - warn "Skipping application startup test" - fi - - log "🎉 Deployment completed successfully!" - info "You can now start the application with: npm start" -} - -# Handle script arguments -case "${1:-deploy}" in - "deploy") - deploy - ;; - "build-only") - log "Running build-only deployment..." - check_env - install_dependencies - run_build - setup_directories - log "Build-only deployment completed" - ;; - "test-connections") - log "Testing connections only..." - check_env - test_database - test_redis - log "Connection tests completed" - ;; - *) - echo "Usage: $0 [deploy|build-only|test-connections]" - echo " deploy - Full deployment (default)" - echo " build-only - Only build, skip tests" - echo " test-connections - Test database and Redis connections" - exit 1 - ;; -esac diff --git a/SerpentRace_Backend/scripts/generate-migration.ts b/SerpentRace_Backend/scripts/generate-migration.ts deleted file mode 100644 index 9ad0a8f3..00000000 --- a/SerpentRace_Backend/scripts/generate-migration.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { execSync } from 'child_process'; - -const migrationName = process.argv[2]; - -if (!migrationName) { - console.error('Please provide a migration name: npm run migration:full '); - process.exit(1); -} - -try { - console.log(`Creating migration: ${migrationName}`); - execSync(`npx ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli migration:create ./src/Infrastructure/Migrationsettings/${migrationName}`, { stdio: 'inherit' }); - - console.log(`Generating migration: ${migrationName}`); - execSync(`npx ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:generate ./src/Infrastructure/Migrations/${migrationName}`, { stdio: 'inherit' }); - - console.log('Migration generated successfully!'); - - console.log('Running migration...'); - execSync(`npx ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./src/Infrastructure/ormconfig.ts migration:run`, { stdio: 'inherit' }); -} catch (error) { - if (error instanceof Error) { - console.error('Migration failed:', error.message); - } else { - console.error('Migration failed:', error); - } - process.exit(1); -} \ No newline at end of file diff --git a/SerpentRace_Backend/scripts/test-redis.ps1 b/SerpentRace_Backend/scripts/test-redis.ps1 deleted file mode 100644 index 18d8ecfa..00000000 --- a/SerpentRace_Backend/scripts/test-redis.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -# PowerShell script to start Redis and run tests -Write-Host "Starting Redis with Docker Compose..." -ForegroundColor Green -docker-compose up -d redis - -# Wait for Redis to be ready -Write-Host "Waiting for Redis to be ready..." -ForegroundColor Yellow -do { - Write-Host "Checking Redis connection..." -ForegroundColor Gray - $result = docker-compose exec redis redis-cli ping 2>$null - if ($result -ne "PONG") { - Start-Sleep -Seconds 2 - } -} while ($result -ne "PONG") - -Write-Host "Redis is ready!" -ForegroundColor Green - -# Run Redis tests -Write-Host "Running Redis tests..." -ForegroundColor Cyan -npm test -- --testNamePattern="RedisService" - -Write-Host "Done!" -ForegroundColor Green diff --git a/SerpentRace_Backend/scripts/test-redis.sh b/SerpentRace_Backend/scripts/test-redis.sh deleted file mode 100644 index 791362ae..00000000 --- a/SerpentRace_Backend/scripts/test-redis.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# Script to start Redis and run tests -echo "Starting Redis with Docker Compose..." -docker-compose up -d redis - -# Wait for Redis to be ready -echo "Waiting for Redis to be ready..." -until docker-compose exec redis redis-cli ping; do - echo "Waiting for Redis..." - sleep 2 -done - -echo "Redis is ready!" - -# Run Redis tests -echo "Running Redis tests..." -npm test -- --testNamePattern="RedisService" - -echo "Done!" diff --git a/SerpentRace_Backend/src/Api/index.ts b/SerpentRace_Backend/src/Api/index.ts deleted file mode 100644 index 34290b3a..00000000 --- a/SerpentRace_Backend/src/Api/index.ts +++ /dev/null @@ -1,284 +0,0 @@ -import express from 'express'; -import { createServer } from 'http'; -import cookieParser from 'cookie-parser'; -import helmet from 'helmet'; -import { AppDataSource } from '../Infrastructure/ormconfig'; -import userRouter from './routers/userRouter'; -import organizationRouter from './routers/organizationRouter'; -import deckRouter from './routers/deckRouter'; -import chatRouter from './routers/chatRouter'; -import contactRouter from './routers/contactRouter'; -import adminRouter from './routers/adminRouter'; -import deckImportExportRouter from './routers/deckImportExportRouter'; -import gameRouter from './routers/gameRouter'; -import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger'; -import { WebSocketService } from '../Application/Services/WebSocketService'; -import { GameWebSocketService } from '../Application/Services/GameWebSocketService'; -import { container } from '../Application/Services/DIContainer'; -import { GameRepository } from '../Infrastructure/Repository/GameRepository'; -import { UserRepository } from '../Infrastructure/Repository/UserRepository'; -import { RedisService } from '../Application/Services/RedisService'; -import { setupSwagger } from './swagger/swaggerUiSetup'; - -const app = express(); -const httpServer = createServer(app); -const PORT = process.env.PORT || 3000; -const isDevelopment = process.env.NODE_ENV === 'development'; - -const loggingService = LoggingService.getInstance(); - -logStartup('SerpentRace Backend starting up', { - environment: process.env.NODE_ENV || 'development', - port: PORT, - nodeVersion: process.version, - chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30' -}); - -app.use(helmet({ - contentSecurityPolicy: isDevelopment ? false : undefined -})); - -app.use(express.json({ limit: '10mb' })); -app.use(express.urlencoded({ extended: true, limit: '10mb' })); -app.use(cookieParser()); - -app.use(loggingService.requestLoggingMiddleware()); - -app.use((req, res, next) => { - const origin = req.headers.origin; - const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080', process.env.FRONTEND_URL]; - - if (!origin || allowedOrigins.includes(origin)) { - res.setHeader('Access-Control-Allow-Origin', origin || '*'); - } - - res.setHeader('Access-Control-Allow-Credentials', 'true'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Cookie'); - - if (req.method === 'OPTIONS') { - res.status(200).end(); - return; - } - - next(); -}); - -if (isDevelopment) { - app.use((req, res, next) => { - logRequest(`${req.method} ${req.path}`, req, res); - next(); - }); -} - -// Setup Swagger documentation -setupSwagger(app); - -app.get('/', (req, res) => { - res.json({ - service: 'SerpentRace Backend API', - status: 'running', - version: '1.0.0', - endpoints: { - swagger: '/api-docs', - users: '/api/users', - organizations: '/api/organizations', - decks: '/api/decks', - chats: '/api/chats', - contacts: '/api/contacts', - admin: '/api/admin', - deckImportExport: '/api/deck-import-export', - health: '/health' - }, - websocket: { - enabled: true, - events: [ - 'chat:join', 'chat:leave', 'message:send', - 'group:create', 'chat:direct', 'game:chat:create', - 'chat:history' - ] - } - }); -}); - -app.get('/health', async (req, res) => { - try { - const isDbConnected = AppDataSource.isInitialized; - - res.json({ - status: 'healthy', - timestamp: new Date().toISOString(), - service: 'SerpentRace Backend API', - version: '1.0.0', - environment: process.env.NODE_ENV || 'development', - database: { - connected: isDbConnected, - type: AppDataSource.options.type - }, - websocket: { - enabled: true - }, - uptime: process.uptime() - }); - } catch (error) { - res.status(503).json({ - status: 'unhealthy', - timestamp: new Date().toISOString(), - error: 'Service health check failed' - }); - } -}); - -// API Routes -app.use('/api/users', userRouter); -app.use('/api/organizations', organizationRouter); -app.use('/api/decks', deckRouter); -app.use('/api/chats', chatRouter); -app.use('/api/contacts', contactRouter); -app.use('/api/admin', adminRouter); -app.use('/api/deck-import-export', deckImportExportRouter); -app.use('/api/games', gameRouter); - -// Global error handler (must be after routes) -app.use(loggingService.errorLoggingMiddleware()); -app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { - logError('Global error handler caught unhandled error', error, req, res); - - // Don't expose internal error details in production - const isDevelopment = process.env.NODE_ENV === 'development'; - - res.status(500).json({ - error: 'Internal server error', - timestamp: new Date().toISOString(), - ...(isDevelopment && { details: error.message, stack: error.stack }) - }); -}); - -// Handle 404 routes -app.use((req: express.Request, res: express.Response) => { - res.status(404).json({ - error: 'Route not found', - path: req.originalUrl, - method: req.method, - timestamp: new Date().toISOString() - }); -}); - -// Initialize WebSocket service after database connection -let webSocketService: WebSocketService; -let gameWebSocketService: GameWebSocketService; - -// Initialize database connection -AppDataSource.initialize() - .then(() => { - const dbOptions = AppDataSource.options as any; - logConnection('Database connection established', 'postgresql', 'success', { - type: dbOptions.type, - host: dbOptions.host, - database: dbOptions.database - }); - - // Initialize WebSocket service after database is connected - webSocketService = new WebSocketService(httpServer); - logStartup('WebSocket service initialized', { - chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30' - }); - - // Initialize Game WebSocket service for /game namespace via DIContainer - container.setSocketIO(webSocketService['io']); - gameWebSocketService = container.gameWebSocketService; - logStartup('Game WebSocket service initialized for /game namespace'); - - // Restore active games from snapshots (if any exist) - gameWebSocketService.restoreAllActiveGames() - .then(restoredCount => { - if (restoredCount > 0) { - logStartup(`Restored ${restoredCount} active game(s) from snapshots`); - } - }) - .catch(error => { - logError('Failed to restore games from snapshots', error); - }); - }) - .catch((error) => { - const dbOptions = AppDataSource.options as any; - logConnection('Database connection failed', 'postgresql', 'failure', { - error: error.message, - type: dbOptions.type, - host: dbOptions.host, - database: dbOptions.database - }); - process.exit(1); - }); - -// Start server with WebSocket support -const server = httpServer.listen(PORT, () => { - logStartup('Server started successfully', { - port: PORT, - environment: process.env.NODE_ENV || 'development', - timestamp: new Date().toISOString(), - endpoints: { - health: `/health`, - swagger: `/api-docs`, - users: `/api/users`, - organizations: `/api/organizations`, - decks: `/api/decks`, - chats: `/api/chats` - }, - websocket: { - enabled: true, - chatInactivityTimeout: `${process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'} minutes` - } - }); -}); - -// Graceful shutdown -const gracefulShutdown = async (signal: string) => { - logStartup(`Received ${signal}. Shutting down gracefully...`); - - // Snapshot all active games before shutdown - if (gameWebSocketService) { - try { - const snapshotCount = await gameWebSocketService.snapshotAllActiveGames(); - logStartup(`Created ${snapshotCount} game snapshot(s) before shutdown`); - } catch (error) { - logError('Failed to snapshot games before shutdown', error as Error); - } - } - - server.close(() => { - logStartup('HTTP server closed'); - - if (AppDataSource.isInitialized) { - AppDataSource.destroy() - .then(() => { - logConnection('Database connection closed', 'postgresql', 'success'); - process.exit(0); - }) - .catch((error) => { - logError('Error during database shutdown', error); - process.exit(1); - }); - } else { - process.exit(0); - } - }); -}; - -process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); -process.on('SIGINT', () => gracefulShutdown('SIGINT')); - -// Handle uncaught exceptions -process.on('uncaughtException', (error) => { - logError('Uncaught Exception - Server will shut down', error); - process.exit(1); -}); - -// Handle unhandled promise rejections -process.on('unhandledRejection', (reason, promise) => { - logError('Unhandled Rejection - Server will shut down', new Error(String(reason)), undefined, undefined); - process.exit(1); -}); - -// Export WebSocket services for game integration -export { webSocketService, gameWebSocketService }; diff --git a/SerpentRace_Backend/src/Api/middleware/optionalAuth.ts b/SerpentRace_Backend/src/Api/middleware/optionalAuth.ts deleted file mode 100644 index 05953518..00000000 --- a/SerpentRace_Backend/src/Api/middleware/optionalAuth.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { JWTService } from '../../Application/Services/JWTService'; -import { UserState } from '../../Domain/User/UserAggregate'; -import { logAuth, logWarning } from '../../Application/Services/Logger'; - -interface AuthenticatedRequest extends Request { - user?: { - userId: string; - authLevel: 0 | 1; - userStatus: UserState; - orgId: string | null; - }; -} - -/** - * Optional authentication middleware - extracts JWT data if present but doesn't require authentication - * Used for endpoints that work for both authenticated and anonymous users - */ -export const optionalAuth = (req: AuthenticatedRequest, res: Response, next: NextFunction) => { - const jwtService = new JWTService(); - - try { - // Try to extract token from Authorization header or cookies - const authHeader = req.headers.authorization; - const token = authHeader?.startsWith('Bearer ') - ? authHeader.substring(7) - : req.cookies?.auth_token; - - if (token) { - // If token exists, try to verify it - const payload = jwtService.verify(req); - - if (payload) { - req.user = { - userId: payload.userId, - authLevel: payload.authLevel, - userStatus: payload.userStatus, - orgId: payload.orgId || null - }; - - logAuth('Optional auth - user authenticated', payload.userId, { - authLevel: payload.authLevel, - userStatus: payload.userStatus, - orgId: payload.orgId - }); - } else { - logWarning('Optional auth - invalid token provided', { - hasToken: true, - tokenLength: token.length - }); - } - } - - // Continue regardless of authentication status - next(); - - } catch (error) { - // Log the error but continue without authentication - logWarning('Optional auth - error processing token', { - error: error instanceof Error ? error.message : String(error), - hasAuthHeader: !!req.headers.authorization, - hasCookie: !!req.cookies?.auth_token - }); - - next(); - } -}; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Api/routers/adminRouter.ts b/SerpentRace_Backend/src/Api/routers/adminRouter.ts deleted file mode 100644 index 4226687a..00000000 --- a/SerpentRace_Backend/src/Api/routers/adminRouter.ts +++ /dev/null @@ -1,1141 +0,0 @@ -import express, { Request, Response } from 'express'; -import multer from 'multer'; -import { DIContainer } from '../../Application/Services/DIContainer'; -import { adminRequired } from '../../Application/Services/AuthMiddleware'; -import { UserState } from '../../Domain/User/UserAggregate'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { AdminAuditService } from '../../Application/Services/AdminBypassService'; -import { webSocketService } from '../index'; -import { logRequest, logError, logWarning, logAuth } from '../../Application/Services/Logger'; - -// Extend Express Request interface for file uploads -declare global { - namespace Express { - interface Request { - file?: Express.Multer.File; - } - } -} - -const router = express.Router(); -const container = DIContainer.getInstance(); - -// Configure multer for file uploads -const upload = multer({ - storage: multer.memoryStorage(), - limits: { - fileSize: 10 * 1024 * 1024, // 10MB limit - }, - fileFilter: (req: any, file: any, cb: any) => { - if (file.mimetype === 'application/json' || file.originalname.endsWith('.spr')) { - cb(null, true); - } else { - cb(new Error('Only JSON and .spr files are allowed')); - } - } -}); - -// Helper function to extract language from Accept-Language header -function extractLanguageFromAcceptHeader(acceptLanguage: string): string | null { - if (!acceptLanguage) return null; - - const languages = acceptLanguage.split(','); - if (languages.length > 0) { - const primaryLanguage = languages[0].split(';')[0].trim().substring(0, 2); - return primaryLanguage; - } - - return null; -} - -// ============================================================================= -// USER MANAGEMENT ROUTES -// ============================================================================= - -// Get users with pagination (RECOMMENDED) -router.get('/users/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ - error: 'Invalid pagination parameters. From and to must be valid numbers with from <= to.' - }); - } - - const limit = to - from + 1; - if (limit > 100) { - return res.status(400).json({ - error: 'Page size too large. Maximum 100 records per request.' - }); - } - - logRequest('Admin paginated users endpoint accessed', req, res, { from, to, includeDeleted }); - - const result = await container.getUsersByPageQueryHandler.execute({ - from, - to, - includeDeleted - }); - - const response = { - users: result.users, - pagination: { - from, - to, - returned: result.users.length, - totalCount: result.totalCount, - includeDeleted - } - }; - - logRequest('Admin users retrieved successfully', req, res, { - returnedUsers: result.users.length, - totalCount: result.totalCount, - from, - to, - includeDeleted - }); - - return res.status(200).json(response); - } catch (error: any) { - logError('Error in admin get users endpoint', error, req, res); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get user by ID including soft-deleted ones -router.get('/users/:userId', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get user by id endpoint accessed', req, res, { targetUserId, includeDeleted }); - - const user = includeDeleted - ? await container.userRepository.findByIdIncludingDeleted(targetUserId) - : await container.userRepository.findById(targetUserId); - - if (!user) { - logWarning('User not found', { targetUserId, includeDeleted }, req, res); - return res.status(404).json({ error: 'User not found' }); - } - - logRequest('Admin user retrieved successfully', req, res, { - targetUserId, - username: user.username, - includeDeleted - }); - - res.json(user); - } catch (error) { - logError('Admin get user by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Search users including soft-deleted ones -router.get('/users/search/:searchTerm', - adminRequired, - ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }), - async (req: Request, res: Response) => { - try { - const { searchTerm } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin search users endpoint accessed', req, res, { searchTerm, includeDeleted }); - - const users = includeDeleted - ? await container.userRepository.searchIncludingDeleted(searchTerm) - : await container.userRepository.search(searchTerm); - - logRequest('Admin user search completed', req, res, { - searchTerm, - resultCount: Array.isArray(users) ? users.length : (users.totalCount || 0), - includeDeleted - }); - - res.json(users); - } catch (error) { - logError('Admin search users endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update any user (admin only) -router.patch('/users/:userId', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const adminUserId = (req as any).user.userId; - - logRequest('Admin update user endpoint accessed', req, res, { - adminUserId, - targetUserId, - fieldsToUpdate: Object.keys(req.body) - }); - - const result = await container.updateUserCommandHandler.execute({ id: targetUserId, ...req.body }); - - if (!result) { - return res.status(404).json({ error: 'User not found' }); - } - - logRequest('User updated by admin', req, res, { - adminUserId, - targetUserId, - username: result.username - }); - - res.json(result); - - } catch (error) { - logError('Admin update user endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('already exists')) { - return res.status(409).json({ error: error.message }); - } - if (error.message.includes('validation')) { - return res.status(400).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Activate user (admin only) -router.post('/users/:userId/activate', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const adminUserId = (req as any).user.userId; - - logRequest('Admin activate user endpoint accessed', req, res, { adminUserId, targetUserId }); - - const result = await container.activateUserCommandHandler.execute({ id: targetUserId }); - - if (!result) { - return res.status(404).json({ error: 'User not found' }); - } - - logAuth('User activated by admin', targetUserId, { adminUserId }, req, res); - res.json({ message: 'User activated successfully', user: result }); - - } catch (error) { - logError('Admin activate user endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Deactivate user (admin only) -router.post('/users/:userId/deactivate', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const adminUserId = (req as any).user.userId; - - logRequest('Deactivate user endpoint accessed', req, res, { adminUserId, targetUserId }); - - const result = await container.deactivateUserCommandHandler.execute({ id: targetUserId }); - - if (!result) { - return res.status(404).json({ error: 'User not found' }); - } - - logAuth('User deactivated by admin', targetUserId, { adminUserId }, req, res); - res.json({ message: 'User deactivated successfully', user: result }); - - } catch (error) { - logError('Deactivate user endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Delete user (admin only) -router.delete('/users/:userId', - adminRequired, - ValidationMiddleware.validateUUIDFormat(['userId']), - async (req: Request, res: Response) => { - try { - const targetUserId = req.params.userId; - const adminUserId = (req as any).user.userId; - const softDelete = req.query.soft === 'true' || req.query.soft === undefined; - - logRequest('Delete user endpoint accessed', req, res, { adminUserId, targetUserId, softDelete }); - - const result = await container.deleteUserCommandHandler.execute({ id: targetUserId, soft: softDelete }); - - if (!result) { - return res.status(404).json({ error: 'User not found' }); - } - - logAuth('User deleted by admin', targetUserId, { adminUserId }, req, res); - res.json({ message: 'User deleted successfully' }); - - } catch (error) { - logError('Delete user endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// DECK MANAGEMENT ROUTES -// ============================================================================= - -// Get decks by page (admin only) - RECOMMENDED -router.get('/decks/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Admin get decks by page endpoint accessed', req, res, { from, to, includeDeleted }); - - // For admin, we need to pass admin context to get unrestricted decks - const adminUserId = (req as any).user.userId; - const result = await container.getDecksByPageQueryHandler.execute({ - userId: adminUserId, - userOrgId: undefined, - isAdmin: true, - from, - to, - includeDeleted - }); - - logRequest('Admin decks page retrieved successfully', req, res, { - from, - to, - count: result.decks.length, - total: result.totalCount, - includeDeleted - }); - - res.json(result); - } catch (error) { - logError('Admin get decks by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get deck by ID including soft-deleted ones -router.get('/decks/:id', adminRequired, async (req: Request, res: Response) => { - try { - const { id } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get deck by id endpoint accessed', req, res, { deckId: id, includeDeleted }); - - const deck = includeDeleted - ? await container.deckRepository.findByIdIncludingDeleted(id) - : await container.deckRepository.findById(id); - - if (!deck) { - logWarning('Deck not found', { deckId: id, includeDeleted }, req, res); - return res.status(404).json({ error: 'Deck not found' }); - } - - logRequest('Admin deck retrieved successfully', req, res, { deckId: id, includeDeleted }); - res.json(deck); - } catch (error) { - logError('Admin get deck by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Search decks including soft-deleted ones -router.get('/decks/search/:searchTerm', adminRequired, async (req: Request, res: Response) => { - try { - const { searchTerm } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin search decks endpoint accessed', req, res, { searchTerm, includeDeleted }); - - const decks = includeDeleted - ? await container.deckRepository.searchIncludingDeleted(searchTerm) - : await container.deckRepository.search(searchTerm); - - logRequest('Admin deck search completed', req, res, { - searchTerm, - resultCount: Array.isArray(decks) ? decks.length : (decks.totalCount || 0), - includeDeleted - }); - - res.json(decks); - } catch (error) { - logError('Admin search decks endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -//modify deck (admin only) -router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) => { - try { - const deckId = req.params.id; - const adminUserId = (req as any).user.userId; - logRequest('Admin update deck endpoint accessed', req, res, { deckId, adminUserId, updateFields: Object.keys(req.body) }); - const result = await container.updateDeckCommandHandler.execute({ id: deckId, userstate: 1 , ...req.body}); - logRequest('Deck updated successfully by admin', req, res, { deckId, adminUserId }); - res.json(result); - } catch (error) { - logError('Admin update deck endpoint error', error as Error, req, res); - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: `Deck not found` }); - } - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) { - return res.status(409).json({ error: 'Deck with this name already exists' }); - } - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Hard delete deck (admin only) -router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => { - try { - const adminUserId = (req as any).user.userId; - const deckId = req.params.id; - logRequest('Admin hard delete deck endpoint accessed', req, res, { deckId }); - - const result = await container.deleteDeckCommandHandler.execute({ userid: adminUserId, authLevel: 1, id: deckId, soft: false }); - - logRequest('Admin deck hard delete successful', req, res, { deckId, success: result }); - res.json({ success: result }); - } catch (error) { - logError('Admin hard delete deck endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Deck not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// ORGANIZATION MANAGEMENT ROUTES -// ============================================================================= - -// Create organization (admin only) -router.post('/organizations', adminRequired, async (req: Request, res: Response) => { - try { - const adminUserId = (req as any).user.userId; - logRequest('Admin create organization endpoint accessed', req, res, { name: req.body.name, adminUserId }); - - const result = await container.createOrganizationCommandHandler.execute(req.body); - - AdminAuditService.logAdminAction('CREATE_ORGANIZATION', adminUserId, { - targetType: 'organization', - targetId: result.id, - operation: 'create', - changes: req.body - }, req, res); - - logRequest('Admin organization created successfully', req, res, { organizationId: result.id, name: req.body.name, adminUserId }); - res.json(result); - } catch (error) { - logError('Admin create organization endpoint error', error as Error, req, res); - - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) { - return res.status(409).json({ error: 'Organization with this name already exists' }); - } - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Update organization (admin only) - NEW ENDPOINT -router.patch('/organizations/:id', adminRequired, async (req: Request, res: Response) => { - try { - const organizationId = req.params.id; - const adminUserId = (req as any).user.userId; - - logRequest('Admin update organization endpoint accessed', req, res, { - adminUserId, - organizationId, - fieldsToUpdate: Object.keys(req.body) - }); - - const result = await container.updateOrganizationCommandHandler.execute({ - id: organizationId, - ...req.body - }); - - if (!result) { - return res.status(404).json({ error: 'Organization not found' }); - } - - AdminAuditService.logAdminAction('UPDATE_ORGANIZATION', adminUserId, { - targetType: 'organization', - targetId: organizationId, - operation: 'update', - changes: req.body, - sensitive: req.body.maxOrganizationalDecks !== undefined - }, req, res); - - logRequest('Organization updated by admin', req, res, { - adminUserId, - organizationId, - organizationName: result.name - }); - - res.json(result); - - } catch (error) { - logError('Admin update organization endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('already exists')) { - return res.status(409).json({ error: error.message }); - } - if (error.message.includes('validation')) { - return res.status(400).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get organizations by page (admin only) - RECOMMENDED -router.get('/organizations/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Admin get organizations by page endpoint accessed', req, res, { from, to, includeDeleted }); - - const result = await container.getOrganizationsByPageQueryHandler.execute({ - from, - to, - includeDeleted - }); - - logRequest('Admin organizations page retrieved successfully', req, res, { - from, - to, - count: result.organizations.length, - total: result.totalCount, - includeDeleted - }); - - res.json(result); - } catch (error) { - logError('Admin get organizations by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get organization by ID including soft-deleted ones -router.get('/organizations/:id', adminRequired, async (req: Request, res: Response) => { - try { - const organizationId = req.params.id; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get organization by id endpoint accessed', req, res, { organizationId, includeDeleted }); - - const organization = includeDeleted - ? await container.organizationRepository.findByIdIncludingDeleted(organizationId) - : await container.organizationRepository.findById(organizationId); - - if (!organization) { - logWarning('Organization not found', { organizationId, includeDeleted }, req, res); - return res.status(404).json({ error: 'Organization not found' }); - } - - logRequest('Admin organization retrieved successfully', req, res, { organizationId, includeDeleted }); - res.json(organization); - } catch (error) { - logError('Admin get organization by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Search organizations including soft-deleted ones -router.get('/organizations/search/:searchTerm', adminRequired, async (req: Request, res: Response) => { - try { - const { searchTerm } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin search organizations endpoint accessed', req, res, { searchTerm, includeDeleted }); - - const organizations = includeDeleted - ? await container.organizationRepository.searchIncludingDeleted(searchTerm) - : await container.organizationRepository.search(searchTerm); - - logRequest('Admin organization search completed', req, res, { - searchTerm, - resultCount: Array.isArray(organizations) ? organizations.length : (organizations.totalCount || 0), - includeDeleted - }); - - res.json(organizations); - } catch (error) { - logError('Admin search organizations endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Soft delete organization (admin only) -router.delete('/organizations/:id', adminRequired, async (req: Request, res: Response) => { - try { - const organizationId = req.params.id; - logRequest('Admin soft delete organization endpoint accessed', req, res, { organizationId }); - - const result = await container.deleteOrganizationCommandHandler.execute({ id: organizationId, soft: true }); - - logRequest('Admin organization soft delete successful', req, res, { organizationId, success: result }); - res.json({ success: result }); - } catch (error) { - logError('Admin soft delete organization endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Organization not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Hard delete organization (admin only) -router.delete('/organizations/:id/hard', adminRequired, async (req: Request, res: Response) => { - try { - const organizationId = req.params.id; - logRequest('Admin hard delete organization endpoint accessed', req, res, { organizationId }); - - const result = await container.deleteOrganizationCommandHandler.execute({ id: organizationId, soft: false }); - - logRequest('Admin organization hard delete successful', req, res, { organizationId, success: result }); - res.json({ success: result }); - } catch (error) { - logError('Admin hard delete organization endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Organization not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// CHAT MANAGEMENT ROUTES -// ============================================================================= - -// Get chats with pagination (RECOMMENDED) -router.get('/chats/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ - error: 'Invalid pagination parameters. From and to must be valid numbers with from <= to.' - }); - } - - const limit = to - from + 1; - if (limit > 100) { - return res.status(400).json({ - error: 'Page size too large. Maximum 100 records per request.' - }); - } - - logRequest('Admin paginated chats endpoint accessed', req, res, { from, to, includeDeleted }); - - const result = await container.getChatsByPageQueryHandler.execute({ - from, - to, - includeDeleted - }); - - const response = { - chats: result.chats, - pagination: { - from, - to, - returned: result.chats.length, - totalCount: result.totalCount, - includeDeleted - } - }; - - logRequest('Admin chats retrieved successfully', req, res, { - returnedChats: result.chats.length, - totalCount: result.totalCount, - from, - to, - includeDeleted - }); - - return res.status(200).json(response); - } catch (error: any) { - logError('Error in admin get chats endpoint', error, req, res); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get chat by ID including soft-deleted ones -router.get('/chats/:id', adminRequired, async (req: Request, res: Response) => { - try { - const { id } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get chat by id endpoint accessed', req, res, { chatId: id, includeDeleted }); - - const chat = includeDeleted - ? await container.chatRepository.findByIdIncludingDeleted(id) - : await container.chatRepository.findById(id); - - if (!chat) { - logWarning('Chat not found', { chatId: id, includeDeleted }, req, res); - return res.status(404).json({ error: 'Chat not found' }); - } - - logRequest('Admin chat retrieved successfully', req, res, { chatId: id, includeDeleted }); - res.json(chat); - } catch (error) { - logError('Admin get chat by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// CONTACT MANAGEMENT ROUTES -// ============================================================================= - -// Get contacts by page (admin only) - RECOMMENDED (already exists, enhanced) -router.get('/contacts/page/:from/:to', adminRequired, async (req: Request, res: Response) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - const includeDeleted = req.query.includeDeleted === 'true'; - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Admin get contacts by page endpoint accessed', req, res, { from, to, includeDeleted }); - - const result = includeDeleted - ? await container.contactRepository.findByPageIncludingDeleted(from, to) - : await container.contactRepository.findByPage(from, to); - - logRequest('Admin contacts page retrieved successfully', req, res, { - from, - to, - count: result.contacts.length, - total: result.totalCount, - includeDeleted - }); - - res.json(result); - } catch (error) { - logError('Admin get contacts by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get contact by ID (admin only) -router.get('/contacts/:id', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin get contact by ID endpoint accessed', req, res, { contactId, includeDeleted }); - - const result = includeDeleted - ? await container.contactRepository.findByIdIncludingDeleted(contactId) - : await container.getContactByIdQueryHandler.execute({ id: contactId }); - - if (!result) { - logRequest('Contact not found', req, res, { contactId, includeDeleted }); - return res.status(404).json({ error: 'Contact not found' }); - } - - logRequest('Admin contact retrieved successfully', req, res, { contactId, includeDeleted }); - res.json(result); - } catch (error) { - logError('Admin get contact by ID endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Search contacts including soft-deleted ones (admin only) -router.get('/contacts/search/:searchTerm', adminRequired, async (req: Request, res: Response) => { - try { - const { searchTerm } = req.params; - const includeDeleted = req.query.includeDeleted === 'true'; - - logRequest('Admin search contacts endpoint accessed', req, res, { searchTerm, includeDeleted }); - - const contacts = includeDeleted - ? await container.contactRepository.searchIncludingDeleted(searchTerm) - : await container.contactRepository.search(searchTerm); - - logRequest('Admin contact search completed', req, res, { - searchTerm, - resultCount: contacts.length, - includeDeleted - }); - - res.json(contacts); - } catch (error) { - logError('Admin search contacts endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Respond to contact (admin only) -router.put('/contacts/:id/respond', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const adminUserId = (req as any).user.userId; - const { adminResponse, sendEmail, language } = req.body; - - if (!adminResponse) { - return res.status(400).json({ error: 'Admin response is required' }); - } - - // Determine language from body, headers, or default to English - let selectedLanguage = language; - if (!selectedLanguage) { - // Try to get language from Accept-Language header - const acceptLanguage = req.headers['accept-language'] as string; - // Try to get language from custom headers (common frontend patterns) - const regionHeader = req.headers['x-region'] as string; - const languageHeader = req.headers['x-language'] as string; - const localeHeader = req.headers['x-locale'] as string; - - selectedLanguage = languageHeader || - localeHeader || - regionHeader || - extractLanguageFromAcceptHeader(acceptLanguage) || - 'en'; - } - - // Validate and normalize language parameter - if (!['en', 'hu', 'de'].includes(selectedLanguage.toLowerCase())) { - selectedLanguage = 'en'; // Fallback to English for unsupported languages - } else { - selectedLanguage = selectedLanguage.toLowerCase(); - } - - logRequest('Admin respond to contact endpoint accessed', req, res, { - contactId, - adminUserId, - sendEmail, - language: selectedLanguage, - headerLanguage: req.headers['accept-language'] || req.headers['x-language'] || 'none' - }); - - // Update contact with response - const result = await container.updateContactCommandHandler.execute({ - id: contactId, - adminResponse, - respondedBy: adminUserId - }); - - if (!result) { - logWarning('Contact not found for response', { contactId }, req, res); - return res.status(404).json({ error: 'Contact not found' }); - } - - // Send email if requested - let emailSent = false; - let emailError = null; - - if (sendEmail === true && adminResponse) { - try { - await container.contactEmailService.sendResponse({ - to: result.email, - message: adminResponse, - contactId: contactId, - adminUserId: adminUserId, - contactName: result.name, - contactType: result.type, - originalMessage: result.txt, - language: selectedLanguage - }); - emailSent = true; - - logRequest('Contact response email sent successfully', req, res, { - contactId, - recipientEmail: result.email, - language: selectedLanguage - }); - } catch (emailErr) { - emailError = emailErr instanceof Error ? emailErr.message : 'Email sending failed'; - logError('Contact response email failed', emailErr as Error, req, res); - } - } - - AdminAuditService.logAdminAction('RESPOND_TO_CONTACT', adminUserId, { - targetType: 'contact', - targetId: contactId, - operation: 'update', - changes: { adminResponse, sendEmail, language: selectedLanguage }, - metadata: { emailSent, emailError } - }, req, res); - - logRequest('Admin contact response saved successfully', req, res, { - contactId, - sendEmail, - emailSent, - language: selectedLanguage - }); - - res.json({ - success: true, - message: 'Response saved successfully', - contact: result, - emailSent, - emailError: emailSent ? null : emailError - }); - } catch (error) { - logError('Admin respond to contact endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Contact not found' }); - } - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Resend contact email (admin only) - NEW ENDPOINT -router.post('/contacts/:id/resend-email', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const adminUserId = (req as any).user.userId; - const { language } = req.body; - - logRequest('Admin resend contact email endpoint accessed', req, res, { - contactId, - adminUserId, - language - }); - - // Get contact details - const contact = await container.getContactByIdQueryHandler.execute({ id: contactId }); - - if (!contact) { - return res.status(404).json({ error: 'Contact not found' }); - } - - if (!contact.adminResponse) { - return res.status(400).json({ error: 'No admin response found to resend' }); - } - - const selectedLanguage = language || 'en'; - - try { - await container.contactEmailService.sendResponse({ - to: contact.email, - message: contact.adminResponse, - contactId: contactId, - adminUserId: adminUserId, - contactName: contact.name, - contactType: contact.type, - originalMessage: contact.txt, - language: selectedLanguage - }); - - AdminAuditService.logAdminAction('RESEND_CONTACT_EMAIL', adminUserId, { - targetType: 'contact', - targetId: contactId, - operation: 'create', - metadata: { language: selectedLanguage, action: 'resend' } - }, req, res); - - logRequest('Contact email resent successfully', req, res, { - contactId, - recipientEmail: contact.email, - language: selectedLanguage - }); - - res.json({ - success: true, - message: 'Email resent successfully' - }); - } catch (emailErr) { - logError('Contact email resend failed', emailErr as Error, req, res); - res.status(500).json({ - error: 'Failed to resend email', - details: emailErr instanceof Error ? emailErr.message : 'Unknown error' - }); - } - } catch (error) { - logError('Admin resend contact email endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Soft delete contact (admin only) - NEW ENDPOINT -router.delete('/contacts/:id', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const adminUserId = (req as any).user.userId; - - logRequest('Admin soft delete contact endpoint accessed', req, res, { contactId, adminUserId }); - - const result = await container.deleteContactCommandHandler.execute({ - id: contactId, - hard: false - }); - - AdminAuditService.logAdminAction('SOFT_DELETE_CONTACT', adminUserId, { - targetType: 'contact', - targetId: contactId, - operation: 'update' - }, req, res); - - logAuth('Contact soft deleted by admin', contactId, { adminUserId }, req, res); - res.json({ success: result }); - } catch (error) { - logError('Admin soft delete contact endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Contact not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Hard delete contact (admin only) - NEW ENDPOINT -router.delete('/contacts/:id/hard', adminRequired, async (req: Request, res: Response) => { - try { - const contactId = req.params.id; - const adminUserId = (req as any).user.userId; - - logRequest('Admin hard delete contact endpoint accessed', req, res, { contactId, adminUserId }); - - const result = await container.deleteContactCommandHandler.execute({ - id: contactId, - hard: true - }); - - AdminAuditService.logAdminAction('HARD_DELETE_CONTACT', adminUserId, { - targetType: 'contact', - targetId: contactId, - operation: 'delete', - sensitive: true - }, req, res); - - logAuth('Contact hard deleted by admin', contactId, { adminUserId }, req, res); - res.json({ success: result }); - } catch (error) { - logError('Admin hard delete contact endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Contact not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// ============================================================================= -// DECK IMPORT/EXPORT ROUTES (ADMIN) -// ============================================================================= - -// Import deck from JSON file (unencrypted, admin only) -router.post('/decks/import', adminRequired, upload.single('file'), async (req: Request, res: Response) => { - try { - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - - const userId = (req as any).user.userId; - const fileContent = req.file!.buffer.toString('utf-8'); - - logRequest('Admin deck import from JSON endpoint accessed', req, res, { fileName: req.file.originalname }); - - let jsonData; - try { - jsonData = JSON.parse(fileContent); - } catch (parseError) { - return res.status(400).json({ error: 'Invalid JSON format' }); - } - - // For admin import, we need to specify both target user and admin user - // Let's assume the deck will be owned by the admin user doing the import - const result = await container.deckImportExportService.adminImportFromJson(jsonData, userId, userId); - - logRequest('Admin deck import successful', req, res, { deckId: result.id, fileName: req.file.originalname }); - - res.json({ - success: true, - message: 'Deck imported successfully', - deckId: result.id - }); - } catch (error) { - logError('Admin deck import from JSON error', error as Error, req, res); - if (error instanceof Error && error.message.includes('Invalid')) { - res.status(400).json({ error: 'Invalid deck data structure' }); - } else { - res.status(500).json({ error: 'Internal server error' }); - } - } -}); - -// Export deck as JSON (unencrypted, admin only) -router.get('/decks/:deckId/export', adminRequired, async (req: Request, res: Response) => { - try { - const { deckId } = req.params; - - logRequest('Admin deck export as JSON endpoint accessed', req, res, { deckId }); - - const deck = await container.deckRepository.findById(deckId); - if (!deck) { - logWarning('Deck not found for export', { deckId }, req, res); - return res.status(404).json({ error: 'Deck not found' }); - } - - logRequest('Admin deck export successful', req, res, { deckId, deckName: deck.name }); - - // Return deck as JSON for admin export - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Content-Disposition', `attachment; filename="${deck.name || 'deck'}.json"`); - res.json(deck); - } catch (error) { - logError('Admin deck export as JSON error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default router; diff --git a/SerpentRace_Backend/src/Api/routers/chatRouter.ts b/SerpentRace_Backend/src/Api/routers/chatRouter.ts deleted file mode 100644 index 140d6791..00000000 --- a/SerpentRace_Backend/src/Api/routers/chatRouter.ts +++ /dev/null @@ -1,287 +0,0 @@ -import express from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { logAuth, logError, logRequest, logWarning } from '../../Application/Services/Logger'; - -const chatRouter = express.Router(); - -// Get user's chats -chatRouter.get('/user-chats', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const includeArchived = req.query.includeArchived === 'true'; - - logRequest('Get user chats endpoint accessed', req, res, { userId, includeArchived }); - - const chats = await container.getUserChatsQueryHandler.execute({ - userId, - includeArchived - }); - - logRequest('User chats retrieved successfully', req, res, { - userId, - chatCount: chats.length - }); - - res.json(chats); - } catch (error) { - logError('Get user chats endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Get chat history -chatRouter.get('/history/:chatId', - authRequired, - ValidationMiddleware.validateUUIDFormat(['chatId']), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const chatId = req.params.chatId; - - logRequest('Get chat history endpoint accessed', req, res, { userId, chatId }); - - const history = await container.getChatHistoryQueryHandler.execute({ - chatId, - userId - }); - - if (!history) { - logWarning('Chat history not found or unauthorized', { userId, chatId }, req, res); - return ErrorResponseService.sendNotFound(res, 'Chat not found or unauthorized'); - } - - logRequest('Chat history retrieved successfully', req, res, { - userId, - chatId, - messageCount: history.messages.length, - isArchived: history.isArchived - }); - - res.json(history); - } catch (error) { - logError('Get chat history endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Create new chat (direct/group) -chatRouter.post('/create', - authRequired, - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['type', 'userIds']), - ValidationMiddleware.validateAllowedValues({ type: ['direct', 'group'] }), - ValidationMiddleware.validateNonEmptyArrays(['userIds']) - ]), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const { type, name, userIds } = req.body; - - logRequest('Create chat endpoint accessed', req, res, { - userId, - type, - targetUserCount: userIds?.length || 0 - }); - - if (type === 'group' && !name?.trim()) { - return ErrorResponseService.sendBadRequest(res, 'Group name is required'); - } - - const chat = await container.createChatCommandHandler.execute({ - type, - name: name?.trim(), - createdBy: userId, - userIds - }); - - if (!chat) { - return ErrorResponseService.sendBadRequest(res, 'Failed to create chat'); - } - - logRequest('Chat created successfully', req, res, { - userId, - chatId: chat.id, - chatType: chat.type - }); - - res.json({ - id: chat.id, - type: chat.type, - name: chat.name, - users: chat.users, - messages: chat.messages - }); - } catch (error) { - logError('Create chat endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Premium subscription required')) { - return ErrorResponseService.sendForbidden(res, 'Premium subscription required to create groups'); - } - if (error.message.includes('not found')) { - return ErrorResponseService.sendNotFound(res, 'One or more users not found'); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Send message (REST endpoint - mainly for testing, real messaging is via WebSocket) -chatRouter.post('/message', - authRequired, - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['chatId', 'message']), - ValidationMiddleware.validateUUIDFormat(['chatId']), - ValidationMiddleware.validateStringLength({ message: { min: 1, max: 2000 } }) - ]), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const { chatId, message } = req.body; - - logRequest('Send message endpoint accessed', req, res, { - userId, - chatId, - messageLength: message?.length || 0 - }); - - const sentMessage = await container.sendMessageCommandHandler.execute({ - chatId, - userId, - message - }); - - if (!sentMessage) { - return ErrorResponseService.sendBadRequest(res, 'Failed to send message'); - } - - logRequest('Message sent successfully', req, res, { - userId, - chatId, - messageId: sentMessage.id - }); - - res.json(sentMessage); - } catch (error) { - logError('Send message endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Chat not found')) { - return ErrorResponseService.sendNotFound(res, 'Chat not found'); - } - if (error.message.includes('not a member')) { - return ErrorResponseService.sendForbidden(res, 'Not authorized to send messages to this chat'); - } - if (error.message.includes('non-empty string')) { - return ErrorResponseService.sendBadRequest(res, 'Message must be a non-empty string'); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Archive chat manually -chatRouter.post('/archive/:chatId', - authRequired, - ValidationMiddleware.validateUUIDFormat(['chatId']), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const chatId = req.params.chatId; - - logRequest('Archive chat endpoint accessed', req, res, { userId, chatId }); - - // Check if user has access to this chat - const chat = await container.chatRepository.findById(chatId); - if (!chat) { - return ErrorResponseService.sendNotFound(res, 'Chat not found'); - } - - if (!chat.users.includes(userId)) { - return ErrorResponseService.sendForbidden(res, 'Not authorized to archive this chat'); - } - - const success = await container.archiveChatCommandHandler.execute({ chatId }); - - if (!success) { - return ErrorResponseService.sendBadRequest(res, 'Failed to archive chat'); - } - - logRequest('Chat archived successfully', req, res, { userId, chatId }); - res.json({ success: true, message: 'Chat archived successfully' }); - - } catch (error) { - logError('Archive chat endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Restore chat from archive -chatRouter.post('/restore/:chatId', - authRequired, - ValidationMiddleware.validateUUIDFormat(['chatId']), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const chatId = req.params.chatId; - - logRequest('Restore chat endpoint accessed', req, res, { userId, chatId }); - - // Check if user has access to this archived chat - const archive = await container.chatArchiveRepository.findByChatId(chatId); - const userArchive = archive.find((a: any) => a.participants.includes(userId)); - - if (!userArchive) { - return ErrorResponseService.sendNotFound(res, 'Archived chat not found or unauthorized'); - } - - const success = await container.restoreChatCommandHandler.execute({ chatId }); - - if (!success) { - return ErrorResponseService.sendBadRequest(res, 'Failed to restore chat (game chats cannot be restored)'); - } - - logRequest('Chat restored successfully', req, res, { userId, chatId }); - res.json({ success: true, message: 'Chat restored successfully' }); - - } catch (error) { - logError('Restore chat endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Get archived chats for a game -chatRouter.get('/archived/game/:gameId', - authRequired, - ValidationMiddleware.validateUUIDFormat(['gameId']), - async (req, res) => { - try { - const userId = (req as any).user.userId; - const gameId = req.params.gameId; - - logRequest('Get archived game chats endpoint accessed', req, res, { userId, gameId }); - - const archivedChats = await container.getArchivedChatsQueryHandler.execute({ - userId, - gameId - }); - - logRequest('Archived game chats retrieved successfully', req, res, { - userId, - gameId, - chatCount: archivedChats.length - }); - - res.json(archivedChats); - } catch (error) { - logError('Get archived game chats endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -export default chatRouter; diff --git a/SerpentRace_Backend/src/Api/routers/contactRouter.ts b/SerpentRace_Backend/src/Api/routers/contactRouter.ts deleted file mode 100644 index d7fcbb53..00000000 --- a/SerpentRace_Backend/src/Api/routers/contactRouter.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Router } from 'express'; -import { container } from '../../Application/Services/DIContainer'; -import { logRequest, logError } from '../../Application/Services/Logger'; -import { ContactType } from '../../Domain/Contact/ContactAggregate'; - -const contactRouter = Router(); - -// Public endpoint - anyone can create a contact -contactRouter.post('/', async (req, res) => { - try { - // Get user ID if authenticated (optional) - const userId = (req as any).user?.userId || null; - - const { name, email, type, txt } = req.body; - - // Validate required fields - if (!name || !email || type === undefined || !txt) { - return res.status(400).json({ - error: 'Missing required fields: name, email, type, and txt are required' - }); - } - - // Validate type - if (!Object.values(ContactType).includes(Number(type))) { - return res.status(400).json({ - error: 'Invalid contact type. Must be one of: 0 (Bug), 1 (Problem), 2 (Question), 3 (Sales), 4 (Other)' - }); - } - - logRequest('Create contact endpoint accessed', req, res, { name, email, type, userId }); - - const result = await container.createContactCommandHandler.execute({ - name, - email, - userid: userId, - type: Number(type), - txt - }); - - logRequest('Contact created successfully', req, res, { contactId: result.id, name, email, type }); - res.status(201).json(result); - } catch (error) { - logError('Create contact endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default contactRouter; diff --git a/SerpentRace_Backend/src/Api/routers/deckImportExportRouter.ts b/SerpentRace_Backend/src/Api/routers/deckImportExportRouter.ts deleted file mode 100644 index cf205d6d..00000000 --- a/SerpentRace_Backend/src/Api/routers/deckImportExportRouter.ts +++ /dev/null @@ -1,124 +0,0 @@ -import express, { Request, Response } from 'express'; -import multer from 'multer'; -import { DIContainer } from '../../Application/Services/DIContainer'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { logRequest, logError, logWarning } from '../../Application/Services/Logger'; - -// Extend Express Request interface for file uploads -declare global { - namespace Express { - interface Request { - file?: Express.Multer.File; - } - } -} - -const router = express.Router(); -const container = DIContainer.getInstance(); - -// Configure multer for file uploads -const upload = multer({ - storage: multer.memoryStorage(), - limits: { - fileSize: 10 * 1024 * 1024, // 10MB limit - }, - fileFilter: (req: any, file: any, cb: any) => { - if (file.mimetype === 'application/json' || file.originalname.endsWith('.spr')) { - cb(null, true); - } else { - cb(new Error('Only JSON and .spr files are allowed')); - } - } -}); - -// Export deck to .spr file (encrypted) - users can only export their own decks -router.get('/export/:deckId', authRequired, async (req: Request, res: Response) => { - try { - const { deckId } = req.params; - const userId = (req as any).user.userId; - - logRequest('Export deck endpoint accessed', req, res, { deckId, userId }); - - // Check if user owns the deck - const deck = await container.deckRepository.findById(deckId); - if (!deck) { - logWarning('Deck not found for export', { deckId, userId }, req, res); - return res.status(404).json({ error: 'Deck not found' }); - } - - // Users can only export their own decks - if (deck.userid !== userId) { - logWarning('Access denied - user attempted to export deck they do not own', { - deckId, - userId, - deckOwnerId: deck.userid - }, req, res); - return res.status(403).json({ error: 'Access denied - you can only export your own decks' }); - } - - const sprData = await container.deckImportExportService.exportDeckToSpr(deckId, userId); - - res.setHeader('Content-Type', 'application/octet-stream'); - res.setHeader('Content-Disposition', `attachment; filename="${deck.name || 'deck'}.spr"`); - - logRequest('Deck exported successfully', req, res, { - deckId, - userId, - deckName: deck.name, - fileSize: sprData.length - }); - - res.send(sprData); - } catch (error) { - logError('Export deck endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Import deck from .spr file (encrypted) - imported deck will be owned by the importing user -router.post('/import', authRequired, upload.single('file'), async (req: Request, res: Response) => { - try { - const userId = (req as any).user.userId; - - logRequest('Import deck endpoint accessed', req, res, { - userId, - hasFile: !!req.file, - fileName: req.file?.originalname, - fileSize: req.file?.size - }); - - if (!req.file) { - logWarning('No file uploaded for deck import', { userId }, req, res); - return res.status(400).json({ error: 'No file uploaded' }); - } - - const fileBuffer = req.file!.buffer; - - // Import the deck and assign ownership to the current user - const result = await container.deckImportExportService.importDeckFromSpr(fileBuffer, userId); - - logRequest('Deck imported successfully', req, res, { - userId, - deckId: result.id, - deckName: result.name || 'Unknown', - fileName: req.file.originalname, - fileSize: req.file.size - }); - - res.json({ - success: true, - message: 'Deck imported successfully and added to your collection', - deckId: result.id - }); - } catch (error) { - logError('Import deck endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('Invalid')) { - return res.status(400).json({ error: 'Invalid file format or corrupted data' }); - } else { - res.status(500).json({ error: 'Internal server error' }); - } - } -}); - -export default router; diff --git a/SerpentRace_Backend/src/Api/routers/deckRouter.ts b/SerpentRace_Backend/src/Api/routers/deckRouter.ts deleted file mode 100644 index 67632a33..00000000 --- a/SerpentRace_Backend/src/Api/routers/deckRouter.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { Router } from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { GeneralSearchService } from '../../Application/Search/Generalsearch'; -import { logRequest, logError, logWarning } from '../../Application/Services/Logger'; -import { Type, CType } from '../../Domain/Deck/DeckAggregate'; - -const deckRouter = Router(); - -/** - * Helper function to convert string enum values to integer enum values - */ -function convertEnumValues(data: any): any { - const converted = { ...data }; - - // Convert Type enum - if (converted.type && typeof converted.type === 'string') { - switch (converted.type.toUpperCase()) { - case 'LUCK': - converted.type = Type.LUCK; - break; - case 'JOKER': - converted.type = Type.JOKER; - break; - case 'QUESTION': - converted.type = Type.QUESTION; - break; - default: - throw new Error('Invalid deck type. Must be LUCK, JOKER, or QUESTION'); - } - } - - // Convert CType enum - if (converted.ctype && typeof converted.ctype === 'string') { - switch (converted.ctype.toUpperCase()) { - case 'PUBLIC': - converted.ctype = CType.PUBLIC; - break; - case 'PRIVATE': - converted.ctype = CType.PRIVATE; - break; - case 'ORGANIZATION': - converted.ctype = CType.ORGANIZATION; - break; - default: - throw new Error('Invalid deck ctype. Must be PUBLIC, PRIVATE, or ORGANIZATION'); - } - } - - return converted; -} - -// Create search service that isn't in the container yet -const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository); - -// Authenticated routes - Get decks with pagination (RECOMMENDED) -deckRouter.get('/page/:from/:to', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const userOrgId = (req as any).user.orgId; - const isAdmin = (req as any).user.authLevel === 1; - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Get decks by page endpoint accessed', req, res, { - userId, - userOrgId, - isAdmin, - from, - to - }); - - // Use paginated query handler for memory efficiency - const result = await container.getDecksByPageQueryHandler.execute({ - userId, - userOrgId, - isAdmin, - from, - to - }); - - logRequest('Get decks page completed successfully', req, res, { - userId, - from, - to, - returnedCount: result.decks.length, - totalCount: result.totalCount - }); - - res.json(result); - } catch (error) { - logError('Get decks by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.post('/', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - logRequest('Create deck endpoint accessed', req, res, { name: req.body.name, userId }); - - // Convert string enum values to integers - const command = convertEnumValues({ - ...req.body, - userid: userId - }); - - const result = await container.createDeckCommandHandler.execute(command); - - logRequest('Deck created successfully', req, res, { deckId: result.id, name: req.body.name, userId }); - res.json(result); - } catch (error) { - logError('Create deck endpoint error', error as Error, req, res); - - // Handle enum validation errors - if (error instanceof Error && error.message.includes('Invalid deck')) { - return res.status(400).json({ error: error.message }); - } - - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) { - return res.status(409).json({ error: 'Deck with this name already exists' }); - } - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.get('/search', authRequired, async (req, res) => { - try { - const { query, limit, offset } = req.query; - logRequest('Search decks endpoint accessed', req, res, { query, limit, offset }); - - if (!query || typeof query !== 'string') { - logWarning('Deck search attempted without query', { query, hasQuery: !!query }, req, res); - return res.status(400).json({ error: 'Search query is required' }); - } - - const searchQuery = { - query: query.trim(), - limit: limit ? parseInt(limit as string) : 20, - offset: offset ? parseInt(offset as string) : 0 - }; - - // Validate pagination parameters - if (searchQuery.limit < 1 || searchQuery.limit > 100) { - logWarning('Invalid deck search limit parameter', { limit: searchQuery.limit }, req, res); - return res.status(400).json({ error: 'Limit must be between 1 and 100' }); - } - - if (searchQuery.offset < 0) { - logWarning('Invalid deck search offset parameter', { offset: searchQuery.offset }, req, res); - return res.status(400).json({ error: 'Offset must be non-negative' }); - } - - const result = await searchService.searchFromUrl(req.originalUrl, searchQuery); - - logRequest('Deck search completed successfully', req, res, { - query: searchQuery.query, - resultCount: Array.isArray(result) ? result.length : 0 - }); - res.json(result); - } catch (error) { - logError('Search decks endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.get('/:id', authRequired, async (req, res) => { - try { - const deckId = req.params.id; - logRequest('Get deck by id endpoint accessed', req, res, { deckId }); - - const result = await container.getDeckByIdQueryHandler.execute({ id: deckId }); - - if (!result) { - logWarning('Deck not found', { deckId }, req, res); - return res.status(404).json({ error: 'Deck not found' }); - } - - logRequest('Deck retrieved successfully', req, res, { deckId }); - res.json(result); - } catch (error) { - logError('Get deck by id endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.patch('/:id', authRequired, async (req, res) => { - try { - const deckId = req.params.id; - const userId = (req as any).user.userId; - const authLevel = (req as any).user.authLevel; - logRequest('Update deck endpoint accessed', req, res, { deckId, userId, updateFields: Object.keys(req.body) }); - - // Convert string enum values to integers - const updateData = convertEnumValues(req.body); - - const result = await container.updateDeckCommandHandler.execute({ userid: userId, authLevel: authLevel, id: deckId, ...updateData }); - - logRequest('Deck updated successfully', req, res, { deckId, userId }); - res.json(result); - } catch (error) { - logError('Update deck endpoint error', error as Error, req, res); - - // Handle enum validation errors - if (error instanceof Error && error.message.includes('Invalid deck')) { - return res.status(400).json({ error: error.message }); - } - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Deck not found' }); - } - - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) { - return res.status(409).json({ error: 'Deck with this name already exists' }); - } - - if (error instanceof Error && error.message.includes('validation')) { - return res.status(400).json({ error: 'Invalid input data', details: error.message }); - } - - if (error instanceof Error && error.message.includes('admin')) { - return res.status(403).json({ error: 'Forbidden: ' + error.message }); - } - - if (error instanceof Error && error.message.includes('admin')) { - return res.status(403).json({ error: 'Forbidden: ' + error.message }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -deckRouter.delete('/:id', authRequired, async (req, res) => { - try { - const deckId = req.params.id; - const userId = (req as any).user.userId; - const authLevel = (req as any).user.authLevel; - logRequest('Soft delete deck endpoint accessed', req, res, { deckId, userId }); - - const result = await container.deleteDeckCommandHandler.execute({ userid: userId, authLevel: authLevel, id: deckId, soft: true }); - - logRequest('Deck soft delete successful', req, res, { deckId, userId, success: result }); - res.json({ success: result }); - } catch (error) { - logError('Soft delete deck endpoint error', error as Error, req, res); - - if (error instanceof Error && error.message.includes('not found')) { - return res.status(404).json({ error: 'Deck not found' }); - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default deckRouter; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Api/routers/gameRouter.ts b/SerpentRace_Backend/src/Api/routers/gameRouter.ts deleted file mode 100644 index 982cf163..00000000 --- a/SerpentRace_Backend/src/Api/routers/gameRouter.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { Router } from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { optionalAuth } from '../middleware/optionalAuth'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { logRequest, logError, logWarning } from '../../Application/Services/Logger'; -import { LoginType } from '../../Domain/Game/GameAggregate'; - -const gameRouter = Router(); - -gameRouter.post('/start', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const orgId = (req as any).user.orgId; - const { deckids, maxplayers, logintype } = req.body; - - logRequest('Start game endpoint accessed', req, res, { - userId, - orgId, - deckCount: deckids?.length, - maxplayers, - logintype - }); - - // Validate required fields - if (!deckids || !Array.isArray(deckids) || deckids.length === 0) { - return res.status(400).json({ error: 'deckids is required and must be a non-empty array' }); - } - - if (!maxplayers || typeof maxplayers !== 'number') { - return res.status(400).json({ error: 'maxplayers is required and must be a number' }); - } - - if (logintype === undefined || typeof logintype !== 'number') { - return res.status(400).json({ error: 'logintype is required and must be a number (0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION)' }); - } - - // Start the game using the GameService - const game = await container.gameService.startGame( - deckids, - maxplayers, - logintype as LoginType, - userId, - orgId - ); - - logRequest('Game started successfully', req, res, { - userId, - gameId: game.id, - gameCode: game.gamecode, - deckCount: game.gamedecks.length, - totalCards: game.gamedecks.reduce((sum, deck) => sum + deck.cards.length, 0) - }); - - res.json(game); - } catch (error) { - logError('Start game endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('not found')) { - return res.status(404).json({ error: error.message }); - } - if (error.message.includes('validation') || - error.message.includes('must be') || - error.message.includes('required') || - error.message.includes('Invalid')) { - return res.status(400).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -gameRouter.post('/join', optionalAuth, async (req, res) => { - try { - const user = (req as any).user; - const { gameCode, playerName } = req.body; - - logRequest('Join game endpoint accessed', req, res, { - gameCode, - playerName, - hasAuth: !!user, - userId: user?.userId, - orgId: user?.orgId - }); - - // Validate required fields - if (!gameCode || typeof gameCode !== 'string') { - return res.status(400).json({ error: 'gameCode is required and must be a string' }); - } - - if (gameCode.length !== 6) { - return res.status(400).json({ error: 'gameCode must be exactly 6 characters long' }); - } - - // First, we need to find the game to determine its type - const gameRepository = container.gameRepository; - const gameToJoin = await gameRepository.findByGameCode(gameCode); - - if (!gameToJoin) { - return res.status(404).json({ error: 'Game not found' }); - } - - // Determine join requirements based on game login type - let actualPlayerId: string | undefined; - let actualPlayerName: string | undefined; - let actualOrgId: string | null = null; - - switch (gameToJoin.logintype) { - case LoginType.PUBLIC: - // Public games: playerName required, authentication optional - // If user is logged in and no playerName provided, use their username - if (!playerName || typeof playerName !== 'string' || !playerName.trim()) { - if (user && user.userId) { - // User is logged in, fetch their username to use as playerName - try { - const userDetails = await container.getUserByIdQueryHandler.execute({ id: user.userId }); - if (userDetails && userDetails.username) { - actualPlayerName = userDetails.username; - logRequest('Using logged-in user\'s username as playerName', req, res, { - userId: user.userId, - username: userDetails.username - }); - } else { - return res.status(400).json({ - error: 'playerName is required for public games' - }); - } - } catch (error) { - logError('Failed to fetch user details for playerName', error as Error, req, res); - return res.status(400).json({ - error: 'playerName is required for public games' - }); - } - } else { - // User is not logged in, playerName is required - return res.status(400).json({ - error: 'playerName is required for public games' - }); - } - } else { - // playerName was provided, use it - actualPlayerName = playerName.trim(); - } - actualPlayerId = user?.userId; // Use authenticated user ID if available, otherwise undefined - break; - - case LoginType.PRIVATE: - // Private games: authentication required - if (!user || !user.userId) { - return res.status(401).json({ - error: 'Authentication required to join private games' - }); - } - actualPlayerId = user.userId; - actualPlayerName = playerName; - break; - - case LoginType.ORGANIZATION: - // Organization games: authentication + organization membership required - if (!user || !user.userId) { - return res.status(401).json({ - error: 'Authentication required to join organization games' - }); - } - - if (!user.orgId) { - return res.status(403).json({ - error: 'Organization membership required to join organization games' - }); - } - - if (gameToJoin.orgid && user.orgId !== gameToJoin.orgid) { - return res.status(403).json({ - error: 'You must be a member of the same organization to join this game' - }); - } - - actualPlayerId = user.userId; - actualPlayerName = playerName; - actualOrgId = user.orgId; - break; - - default: - return res.status(400).json({ error: 'Invalid game type' }); - } - - // Join the game using the GameService with determined parameters - const game = await container.gameService.joinGame( - gameCode, - actualPlayerId, - actualPlayerName, - actualOrgId, - gameToJoin.logintype - ); - - logRequest('Player joined game successfully', req, res, { - userId: actualPlayerId || 'anonymous', - gameId: game.id, - gameCode: game.gamecode, - gameType: LoginType[gameToJoin.logintype], - playerCount: game.players.length, - maxPlayers: game.maxplayers, - playerName: actualPlayerName - }); - - // Create game token for WebSocket authentication - const gameTokenService = container.gameTokenService; - const gameToken = gameTokenService.createGameToken( - game.id, - game.gamecode, - actualPlayerName || 'Anonymous', - actualPlayerId - ); - - // Return clean response with essential data + game token - res.json({ - id: game.id, - gamecode: game.gamecode, - playerName: actualPlayerName, - playerCount: game.players.length, - maxPlayers: game.maxplayers, - gameType: LoginType[gameToJoin.logintype], - isAuthenticated: !!actualPlayerId, - gameToken: gameToken - }); - } catch (error) { - logError('Join game endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('not found')) { - return res.status(404).json({ error: error.message }); - } - if (error.message.includes('Authentication required')) { - return res.status(401).json({ error: error.message }); - } - if (error.message.includes('Organization') || error.message.includes('organization')) { - return res.status(403).json({ error: error.message }); - } - if (error.message.includes('full') || - error.message.includes('already in') || - error.message.includes('not accepting')) { - return res.status(409).json({ error: error.message }); - } - if (error.message.includes('validation') || - error.message.includes('must be') || - error.message.includes('required') || - error.message.includes('Invalid')) { - return res.status(400).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -gameRouter.post('/:gameId/start', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const { gameId } = req.params; - - logRequest('Start gameplay endpoint accessed', req, res, { - userId, - gameId - }); - - // Validate required fields - if (!gameId || typeof gameId !== 'string') { - return res.status(400).json({ error: 'gameId is required and must be a string' }); - } - - // Start the gameplay using the GameService - const result = await container.gameService.startGamePlay(gameId, userId); - - logRequest('Game gameplay started successfully', req, res, { - userId, - gameId, - playerCount: result.game.players.length - }); - - res.json({ - message: 'Game started successfully', - gameId: gameId, - playerCount: result.game.players.length, - game: result.game, - boardData: result.boardData - }); - } catch (error) { - logError('Start gameplay endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('not found')) { - return res.status(404).json({ error: error.message }); - } - if (error.message.includes('Only') || error.message.includes('master')) { - return res.status(403).json({ error: error.message }); - } - if (error.message.includes('already started') || - error.message.includes('not ready') || - error.message.includes('minimum players') || - error.message.includes('not in waiting state') || - error.message.includes('cannot be started')) { - return res.status(409).json({ error: error.message }); - } - if (error.message.includes('validation') || - error.message.includes('must be') || - error.message.includes('required') || - error.message.includes('Invalid')) { - return res.status(400).json({ error: error.message }); - } - // Board generation specific errors - if (error.message.includes('Board generation') || - error.message.includes('board not found') || - error.message.includes('BoardGenerationService') || - error.message.includes('Failed to wait for board generation') || - error.message.includes('board generation timeout')) { - return res.status(500).json({ error: error.message }); - } - } - - res.status(500).json({ error: 'Internal server error' }); - } -}); - -export default gameRouter; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Api/routers/organizationRouter.ts b/SerpentRace_Backend/src/Api/routers/organizationRouter.ts deleted file mode 100644 index eceaad01..00000000 --- a/SerpentRace_Backend/src/Api/routers/organizationRouter.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { Router } from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { GeneralSearchService } from '../../Application/Search/Generalsearch'; -import { logRequest, logError, logWarning, logAuth } from '../../Application/Services/Logger'; - -const organizationRouter = Router(); - -// Create search service that isn't in the container yet -const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository); - -// Auth routes - Get organizations with pagination (RECOMMENDED) -organizationRouter.get('/page/:from/:to', authRequired, async (req, res) => { - try { - const from = parseInt(req.params.from); - const to = parseInt(req.params.to); - - if (isNaN(from) || isNaN(to) || from < 0 || to < from) { - return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' }); - } - - logRequest('Get organizations by page endpoint accessed', req, res, { from, to }); - - const result = await container.getOrganizationsByPageQueryHandler.execute({ from, to }); - - logRequest('Organizations page retrieved successfully', req, res, { - from, - to, - count: result.organizations.length, - totalCount: result.totalCount - }); - - res.json(result); - } catch (error) { - logError('Get organizations by page endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -organizationRouter.get('/search', authRequired, async (req, res) => { - try { - const { query, limit, offset } = req.query; - logRequest('Search organizations endpoint accessed', req, res, { query, limit, offset }); - - if (!query || typeof query !== 'string') { - logWarning('Organization search attempted without query', { query, hasQuery: !!query }, req, res); - return res.status(400).json({ error: 'Search query is required' }); - } - - const searchQuery = { - query: query.trim(), - limit: limit ? parseInt(limit as string) : 20, - offset: offset ? parseInt(offset as string) : 0 - }; - - // Validate pagination parameters - if (searchQuery.limit < 1 || searchQuery.limit > 100) { - logWarning('Invalid organization search limit parameter', { limit: searchQuery.limit }, req, res); - return res.status(400).json({ error: 'Limit must be between 1 and 100' }); - } - - if (searchQuery.offset < 0) { - logWarning('Invalid organization search offset parameter', { offset: searchQuery.offset }, req, res); - return res.status(400).json({ error: 'Offset must be non-negative' }); - } - - const result = await searchService.searchFromUrl(req.originalUrl, searchQuery); - - logRequest('Organization search completed successfully', req, res, { - query: searchQuery.query, - resultCount: Array.isArray(result) ? result.length : 0 - }); - res.json(result); - } catch (error) { - logError('Search organizations endpoint error', error as Error, req, res); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Get organization login URL -organizationRouter.get('/:orgId/login-url', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const { orgId } = req.params; - - logRequest('Get organization login URL endpoint accessed', req, res, { - userId, - organizationId: orgId - }); - - const result = await container.getOrganizationLoginUrlQueryHandler.execute({ - organizationId: orgId - }); - - if (!result) { - logWarning('Organization login URL not found', { - organizationId: orgId, - userId - }, req, res); - return ErrorResponseService.sendNotFound(res, 'Organization login URL not found'); - } - - logRequest('Organization login URL retrieved successfully', req, res, { - organizationId: orgId, - organizationName: result.organizationName, - hasUrl: !!result.loginUrl, - userId - }); - - res.json(result); - } catch (error) { - logError('Get organization login URL endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Process third-party authentication callback -organizationRouter.post('/auth-callback', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const { organizationId, status, authToken } = req.body; - - logRequest('Organization auth callback endpoint accessed', req, res, { - userId, - organizationId, - status, - hasAuthToken: !!authToken - }); - - // Validate required fields - if (!organizationId || !status) { - logWarning('Missing required fields for organization auth callback', { - organizationId: !!organizationId, - status: !!status, - userId - }, req, res); - return ErrorResponseService.sendBadRequest(res, 'organizationId and status are required'); - } - - if (status !== 'ok' && status !== 'not_ok') { - logWarning('Invalid status value for organization auth callback', { - status, - userId, - organizationId - }, req, res); - return ErrorResponseService.sendBadRequest(res, 'status must be either "ok" or "not_ok"'); - } - - const result = await container.processOrgAuthCallbackCommandHandler.execute({ - organizationId, - userId, - status, - authToken - }); - - if (!result.success) { - if (result.message.includes('not found')) { - logWarning('Organization auth callback failed - entity not found', { - userId, - organizationId, - message: result.message - }, req, res); - return ErrorResponseService.sendNotFound(res, result.message); - } - if (result.message.includes('does not belong')) { - logWarning('Organization auth callback failed - authorization error', { - userId, - organizationId, - message: result.message - }, req, res); - return ErrorResponseService.sendForbidden(res, result.message); - } - if (result.message.includes('authentication failed')) { - logAuth('Organization authentication failed via callback', userId, { - organizationId, - status - }, req, res); - return ErrorResponseService.sendUnauthorized(res, result.message); - } - - logError('Organization auth callback internal error', new Error(result.message), req, res); - return ErrorResponseService.sendInternalServerError(res); - } - - logAuth('Organization auth callback processed successfully', userId, { - organizationId, - status, - updatedFields: result.updatedFields - }, req, res); - - res.json({ - success: result.success, - message: result.message, - updatedFields: result.updatedFields - }); - } catch (error) { - logError('Organization auth callback endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -export default organizationRouter; diff --git a/SerpentRace_Backend/src/Api/routers/userRouter.ts b/SerpentRace_Backend/src/Api/routers/userRouter.ts deleted file mode 100644 index facc906d..00000000 --- a/SerpentRace_Backend/src/Api/routers/userRouter.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { Router } from 'express'; -import { authRequired } from '../../Application/Services/AuthMiddleware'; -import { container } from '../../Application/Services/DIContainer'; -import { ErrorResponseService } from '../../Application/Services/ErrorResponseService'; -import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware'; -import { GeneralSearchService } from '../../Application/Search/Generalsearch'; -import { logRequest, logError, logAuth, logWarning } from '../../Application/Services/Logger'; - -const userRouter = Router(); - -// Create search service that isn't in the container yet -const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository); - -// Login endpoint -userRouter.post('/login', - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['username', 'password']), - ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 50 }, - password: { min: 6, max: 100 } - }) - ]), - async (req, res) => { - try { - logRequest('Login endpoint accessed', req, res, { username: req.body.username }); - - const { username, password } = req.body; - - const result = await container.loginCommandHandler.execute({ username, password }, res); - - if (result) { - logAuth('User login successful', undefined, { username: result.user.username }, req, res); - res.json(result); - } else { - throw new Error(`Login failed: ${result}`); - } - - } catch (error) { - logError('Login endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Invalid username')) { - return ErrorResponseService.sendUnauthorized(res, 'Invalid username or password'); - } - if (error.message.includes('Invalid password')) { - return ErrorResponseService.sendUnauthorized(res, 'Invalid username or password'); - } - if (error.message.includes('not verified')) { - return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address'); - } - if (error.message.includes('restriction')) { - return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address'); - } - if (error.message.includes('deactivated')) { - return ErrorResponseService.sendUnauthorized(res, 'Account has been deactivated'); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Create user endpoint -userRouter.post('/create', - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['username', 'email', 'password']), - ValidationMiddleware.validateEmailFormat(['email']), - ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 50 }, - password: { min: 6, max: 100 } - }) - ]), - async (req, res) => { - try { - logRequest('Create user endpoint accessed', req, res, { - username: req.body.username, - email: req.body.email - }); - - const acceptLanguage = req.header('Accept-Language') || 'en'; - const language : 'hu' | 'de' | 'en' = acceptLanguage.toLowerCase().startsWith('hu') ? 'hu' : - acceptLanguage.toLowerCase().startsWith('de') ? 'de' : 'en'; - - const result = await container.createUserCommandHandler.execute({ ...req.body, language }); - - logRequest('User created successfully', req, res, { - username: result.username - }); - - res.status(201).json(result); - - } catch (error) { - // Don't log here since CreateUserCommandHandler already logs system errors - // Only log validation/user input errors at router level - - if (error instanceof Error) { - if (error.message.includes('already exists')) { - return ErrorResponseService.sendConflict(res, error.message); - } - if (error.message.includes('validation')) { - return ErrorResponseService.sendBadRequest(res, error.message); - } - // Log unexpected errors that weren't handled by the command handler - if (!error.message.includes('Failed to create user')) { - logError('Unexpected create user endpoint error', error as Error, req, res); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Get user profile (current user) -userRouter.get('/profile', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - - logRequest('Get user profile endpoint accessed', req, res, { userId }); - - const result = await container.getUserByIdQueryHandler.execute({ id: userId }); - - if (!result) { - logWarning('User profile not found', { userId }, req, res); - return ErrorResponseService.sendNotFound(res, 'User not found'); - } - - logRequest('User profile retrieved successfully', req, res, { - userId, - username: result.username - }); - - res.json(result); - - } catch (error) { - logError('Get user profile endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Update user profile (current user) -userRouter.patch('/profile', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - - logRequest('Update user profile endpoint accessed', req, res, { - userId, - fieldsToUpdate: Object.keys(req.body) - }); - - const result = await container.updateUserCommandHandler.execute({ id: userId, ...req.body }); - - if (!result) { - return ErrorResponseService.sendNotFound(res, 'User not found'); - } - - logRequest('User profile updated successfully', req, res, { - userId, - username: result.username - }); - - res.json(result); - - } catch (error) { - logError('Update user profile endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('already exists')) { - return ErrorResponseService.sendConflict(res, error.message); - } - if (error.message.includes('validation')) { - return ErrorResponseService.sendBadRequest(res, error.message); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -//Soft delete user (current user) -userRouter.delete('/profile', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - const result = await container.deleteUserCommandHandler.execute({ id: userId, soft: true }); - logRequest('User soft deleted successfully', req, res, { userId }); - res.json({ success: result }); - } catch (error) { - logError('Soft delete user endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -//logout user (current user) -userRouter.post('/logout', authRequired, async (req, res) => { - try { - const userId = (req as any).user.userId; - await container.logoutCommandHandler.execute(userId, res, req); - logRequest('User logged out successfully', req, res, { userId }); - res.json({ success: true }); - } catch (error) { - logError('Logout user endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Refresh token endpoint -userRouter.post('/refresh-token', async (req, res) => { - try { - logRequest('Token refresh endpoint accessed', req, res); - - const jwtService = container.jwtService; - const newTokenPair = jwtService.attemptTokenRefresh(req, res); - - if (newTokenPair) { - logRequest('Token refresh successful', req, res); - res.json({ - success: true, - message: 'Tokens refreshed successfully', - accessToken: newTokenPair.accessToken, - refreshToken: newTokenPair.refreshToken - }); - } else { - logWarning('Token refresh failed - invalid or missing refresh token', undefined, req, res); - return ErrorResponseService.sendUnauthorized(res, 'Invalid or expired refresh token'); - } - } catch (error) { - logError('Refresh token endpoint error', error as Error, req, res); - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Email verification endpoint -userRouter.post('/verify-email/:token', async (req, res) => { - try { - const { token } = req.params; - - logRequest('Email verification endpoint accessed', req, res, { - tokenPrefix: token.substring(0, 8) + '...' - }); - - if (!token) { - return ErrorResponseService.sendBadRequest(res, 'Verification token is required'); - } - - const result = await container.verifyEmailCommandHandler.execute({ token }); - - if (result) { - logAuth('Email verification successful', undefined, { tokenPrefix: token.substring(0, 8) + '...' }, req, res); - res.json({ success: true, message: 'Email verified successfully' }); - } else { - throw new Error('Email verification failed'); - } - - } catch (error) { - logError('Email verification endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Invalid') || error.message.includes('expired')) { - return ErrorResponseService.sendBadRequest(res, 'Invalid or expired verification token'); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -// Forgot password request endpoint -userRouter.post('/forgot-password', - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['email']), - ValidationMiddleware.validateEmailFormat(['email']) - ]), - async (req, res) => { - try { - const { email } = req.body; - const acceptLanguage = req.header('Accept-Language') || 'en'; - const language: 'hu' | 'de' | 'en' = acceptLanguage.toLowerCase().startsWith('hu') ? 'hu' : - acceptLanguage.toLowerCase().startsWith('de') ? 'de' : 'en'; - - logRequest('Forgot password endpoint accessed', req, res, { email }); - - const result = await container.requestPasswordResetCommandHandler.execute({ language, email }); - - if (result) { - logAuth('Password reset request successful', undefined, { email }, req, res); - res.json({ - success: true, - message: 'If an account with this email exists, a password reset link has been sent' - }); - } else { - throw new Error('Password reset request failed'); - } - - } catch (error) { - logError('Forgot password endpoint error', error as Error, req, res); - - // Always return success for security (don't reveal if email exists) - res.json({ - success: true, - message: 'If an account with this email exists, a password reset link has been sent' - }); - } -}); - -// Reset password endpoint -userRouter.post('/reset-password', - ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['token', 'newPassword']), - ValidationMiddleware.validateStringLength({ - newPassword: { min: 6, max: 100 } - }) - ]), - async (req, res) => { - try { - const { token, newPassword } = req.body; - - logRequest('Reset password endpoint accessed', req, res, { - tokenPrefix: token.substring(0, 8) + '...' - }); - - const result = await container.resetPasswordCommandHandler.execute({ token, newPassword }); - - if (result) { - logAuth('Password reset successful', undefined, { tokenPrefix: token.substring(0, 8) + '...' }, req, res); - res.json({ success: true, message: 'Password reset successfully' }); - } else { - throw new Error('Password reset failed'); - } - - } catch (error) { - logError('Reset password endpoint error', error as Error, req, res); - - if (error instanceof Error) { - if (error.message.includes('Invalid') || error.message.includes('expired')) { - return ErrorResponseService.sendBadRequest(res, 'Invalid or expired reset token'); - } - if (error.message.includes('Password validation')) { - return ErrorResponseService.sendBadRequest(res, error.message); - } - } - - return ErrorResponseService.sendInternalServerError(res); - } -}); - -export default userRouter; diff --git a/SerpentRace_Backend/src/Api/swagger/swaggerConfig.ts b/SerpentRace_Backend/src/Api/swagger/swaggerConfig.ts deleted file mode 100644 index 1f2e31ef..00000000 --- a/SerpentRace_Backend/src/Api/swagger/swaggerConfig.ts +++ /dev/null @@ -1,101 +0,0 @@ -import swaggerJSDoc from 'swagger-jsdoc'; -import path from 'path'; - -export const swaggerOptions = { - definition: { - openapi: '3.0.0', - info: { - title: 'SerpentRace API', - version: '1.0.0', - description: 'Comprehensive API documentation for SerpentRace Backend', - contact: { - name: 'SerpentRace Development Team', - email: 'dev@serpentrace.com' - }, - license: { - name: 'MIT', - url: 'https://opensource.org/licenses/MIT' - } - }, - servers: [ - { - url: 'http://localhost:3001', - description: 'Local development server' - }, - { - url: 'http://localhost:3000', - description: 'Local development server (alt)' - }, - { - url: 'https://api.serpentrace.com', - description: 'Production server' - } - ], - components: { - securitySchemes: { - bearerAuth: { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - description: 'Enter JWT token obtained from /api/users/login' - } - } - }, - security: [{ bearerAuth: [] }], - tags: [ - { - name: 'Users', - description: 'User authentication and profile management' - }, - { - name: 'Organizations', - description: 'Organization management and authentication' - }, - { - name: 'Decks', - description: 'Deck creation, management, and gameplay' - }, - { - name: 'Chats', - description: 'Real-time chat and messaging system' - }, - { - name: 'Contacts', - description: 'Contact form and support requests' - }, - { - name: 'Deck Import/Export', - description: 'Import and export deck functionality' - }, - { - name: 'Games', - description: 'Game management and gameplay' - }, - { - name: 'Admin - Users', - description: 'Admin user management operations' - }, - { - name: 'Admin - Decks', - description: 'Admin deck management operations' - }, - { - name: 'Admin - Organizations', - description: 'Admin organization management operations' - }, - { - name: 'Admin - Chats', - description: 'Admin chat management operations' - }, - { - name: 'Admin - Contacts', - description: 'Admin contact management operations' - } - ] - }, - apis: [ - './src/Api/swagger/swaggerDefinitionsFixed.ts' - ], -}; - -export const swaggerSpec = swaggerJSDoc(swaggerOptions); diff --git a/SerpentRace_Backend/src/Api/swagger/swaggerDefinitions.ts b/SerpentRace_Backend/src/Api/swagger/swaggerDefinitions.ts deleted file mode 100644 index eef90dac..00000000 --- a/SerpentRace_Backend/src/Api/swagger/swaggerDefinitions.ts +++ /dev/null @@ -1,1694 +0,0 @@ -/** - * @swagger - * components: -<<<<<<< HEAD -<<<<<<< HEAD -======= - * securitySchemes: - * bearerAuth: - * type: http - * scheme: bearer - * bearerFormat: JWT ->>>>>>> origin/main -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * schemas: - * User: - * type: object - * properties: - * id: - * type: string - * format: uuid - * username: - * type: string - * email: - * type: string - * format: email - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * nullable: true - * type: - * type: string - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * orgid: - * type: string - * nullable: true - * - * CreateUserRequest: - * type: object - * required: - * - username - * - email - * - password - * - fname - * - lname - * - type - * properties: - * username: - * type: string - * email: - * type: string - * format: email - * password: - * type: string - * format: password - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * type: - * type: string - * - * LoginRequest: - * type: object - * required: - * - username - * - password - * properties: - * username: - * type: string - * password: - * type: string - * format: password - * - * LoginResponse: - * type: object - * properties: - * token: - * type: string - * user: - * $ref: '#/components/schemas/User' - * requiresOrgReauth: - * type: boolean - * orgLoginUrl: - * type: string - * organizationName: - * type: string - * - * UpdateProfileRequest: - * type: object - * properties: - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * email: - * type: string - * format: email - * -<<<<<<< HEAD - * ForgotPasswordRequest: - * type: object - * required: - * - email - * properties: - * email: - * type: string - * format: email - * - * ResetPasswordRequest: - * type: object - * required: - * - token - * - newPassword - * properties: - * token: - * type: string - * newPassword: - * type: string - * format: password - * minLength: 6 - * maxLength: 100 - * - * AuthSuccessResponse: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * -======= ->>>>>>> origin/main - * Organization: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * contactfname: - * type: string - * contactlname: - * type: string - * contactphone: - * type: string - * contactemail: - * type: string - * format: email - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * url: - * type: string - * nullable: true - * userinorg: - * type: integer - * maxOrganizationalDecks: - * type: integer - * - * Deck: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * enum: [0, 1, 2, 3] - * description: 0=JOKER, 1=LUCK, 2=QUESTION, 3=GENERAL - * userid: - * type: string - * format: uuid - * creationdate: - * type: string - * format: date-time - * cards: - * type: array - * items: - * type: object - * playedNumber: - * type: integer - * ctype: - * type: integer - * enum: [0, 1, 2] - * description: 0=PUBLIC, 1=ORGANIZATIONAL, 2=PRIVATE - * updatedate: - * type: string - * format: date-time - * state: - * type: integer - * enum: [0, 1, 2] - * description: 0=ACTIVE, 1=INACTIVE, 2=SOFT_DELETE - * organization: - * $ref: '#/components/schemas/Organization' - * nullable: true - * - * CreateDeckRequest: - * type: object - * required: - * - name - * - type - * - cards - * properties: - * name: - * type: string - * type: - * type: integer - * cards: - * type: array - * items: - * type: object - * ctype: - * type: integer - * - * Contact: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * email: - * type: string - * format: email - * userid: - * type: string - * format: uuid - * nullable: true - * type: - * type: integer - * enum: [0, 1, 2] - * description: 0=QUESTION, 1=BUG_REPORT, 2=SUGGESTION - * txt: - * type: string - * state: - * type: integer - * createDate: - * type: string - * format: date-time - * updateDate: - * type: string - * format: date-time - * adminResponse: - * type: string - * nullable: true - * responseDate: - * type: string - * format: date-time - * nullable: true - * respondedBy: - * type: string - * nullable: true - * - * CreateContactRequest: - * type: object - * required: - * - name - * - email - * - type - * - txt - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * type: - * type: integer - * txt: - * type: string - * - * Chat: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * participants: - * type: array - * items: - * type: string - * creatorId: - * type: string - * gameId: - * type: string - * nullable: true - * createDate: - * type: string - * format: date-time - * updateDate: - * type: string - * format: date-time - * state: - * type: integer - * - * ChatMessage: - * type: object - * properties: - * id: - * type: string - * format: uuid - * senderId: - * type: string - * senderName: - * type: string - * message: - * type: string - * timestamp: - * type: string - * format: date-time - * chatId: - * type: string - * -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * Game: - * type: object - * properties: - * id: - * type: string - * format: uuid - * gamecode: - * type: string - * maxplayers: - * type: integer - * logintype: - * type: integer - * gamedecks: - * type: array - * players: - * type: array - * items: - * type: string - * started: - * type: boolean - * finished: - * type: boolean - * state: - * type: integer - * createdate: - * type: string - * format: date-time - * -<<<<<<< HEAD -======= ->>>>>>> origin/main -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * Error: - * type: object - * properties: - * error: - * type: string - * timestamp: - * type: string - * format: date-time - * details: - * type: string -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - */ -/** - * @swagger - * - * /api/users/login: - * post: - * tags: [Users] - * summary: User login - * description: Authenticate user and return JWT token - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginRequest' - * responses: - * 200: - * description: Login successful -<<<<<<< HEAD - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginResponse' - * 401: - * description: Invalid credentials - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * -======= - * - * paths: - * /api/users/login: - * post: - * tags: [Users] - * summary: User login - * description: Authenticate user and return JWT token - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginRequest' - * responses: - * 200: - * description: Login successful - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginResponse' - * 401: - * description: Invalid credentials - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' ->>>>>>> origin/main -======= - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginResponse' - * 401: - * description: Invalid credentials - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * - * /api/users/create: - * post: - * tags: [Users] - * summary: Create new user - * description: Register a new user account - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateUserRequest' - * responses: - * 201: - * description: User created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 400: - * description: Validation error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Error' - * - * /api/users/profile: - * get: - * tags: [Users] - * summary: Get user profile - * description: Get current user's profile information - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: User profile data - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 401: - * description: Unauthorized - * patch: - * tags: [Users] - * summary: Update user profile - * description: Update current user's profile information - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/UpdateProfileRequest' - * responses: - * 200: - * description: Profile updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 400: - * description: Validation error - * 401: - * description: Unauthorized - * - * /api/organizations/page/{from}/{to}: - * get: - * tags: [Organizations] - * summary: Get organizations by page - * description: Retrieve paginated list of organizations - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated organizations - * content: - * application/json: - * schema: - * type: object - * properties: - * organizations: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * totalCount: - * type: integer - * - * /api/organizations/search: - * get: - * tags: [Organizations] - * summary: Search organizations - * description: Search organizations by query - * security: - * - bearerAuth: [] - * parameters: - * - name: query - * in: query - * required: true - * schema: - * type: string - * - name: from - * in: query - * required: true - * schema: - * type: integer - * - name: to - * in: query - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: object - * properties: - * results: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * totalCount: - * type: integer - * - * /api/organizations/{orgId}/login-url: - * get: - * tags: [Organizations] - * summary: Get organization login URL - * description: Get OAuth login URL for organization - * security: - * - bearerAuth: [] - * parameters: - * - name: orgId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Login URL - * content: - * application/json: - * schema: - * type: object - * properties: - * loginUrl: - * type: string - * - * /api/organizations/auth-callback: - * post: - * tags: [Organizations] - * summary: OAuth callback - * description: Handle OAuth callback from organization - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * code: - * type: string - * state: - * type: string - * responses: - * 200: - * description: Authentication successful - * 400: - * description: Invalid callback data - * - * /api/decks/page/{from}/{to}: - * get: - * tags: [Decks] - * summary: Get decks by page - * description: Retrieve paginated list of decks - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated decks - * content: - * application/json: - * schema: - * type: object - * properties: - * decks: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - * - * /api/decks: - * post: - * tags: [Decks] - * summary: Create deck - * description: Create a new deck - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateDeckRequest' - * responses: - * 201: - * description: Deck created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * - * /api/decks/search: - * get: - * tags: [Decks] - * summary: Search decks - * description: Search decks by query - * security: - * - bearerAuth: [] - * parameters: - * - name: query - * in: query - * required: true - * schema: - * type: string - * - name: from - * in: query - * required: true - * schema: - * type: integer - * - name: to - * in: query - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: object - * properties: - * results: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - * - * /api/decks/{id}: - * get: - * tags: [Decks] - * summary: Get deck by ID - * description: Retrieve a specific deck by ID - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * 404: - * description: Deck not found - * put: - * tags: [Decks] - * summary: Update deck - * description: Update an existing deck - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateDeckRequest' - * responses: - * 200: - * description: Deck updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * delete: - * tags: [Decks] - * summary: Delete deck - * description: Delete a deck (soft delete) - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Deck deleted successfully - * 404: - * description: Deck not found - * - * /api/chats/user-chats: - * get: - * tags: [Chats] - * summary: Get user chats - * description: Get all chats for the current user - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: User chats - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - * - * /api/chats/history/{chatId}: - * get: - * tags: [Chats] - * summary: Get chat history - * description: Get message history for a chat - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * - name: page - * in: query - * schema: - * type: integer - * - name: limit - * in: query - * schema: - * type: integer - * responses: - * 200: - * description: Chat history - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - * - * /api/chats/create: - * post: - * tags: [Chats] - * summary: Create chat - * description: Create a new chat room - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - gameId - * properties: - * name: - * type: string - * gameId: - * type: string - * password: - * type: string - * nullable: true - * responses: - * 201: - * description: Chat created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Chat' - * - * /api/chats/message: - * post: - * tags: [Chats] - * summary: Send message - * description: Send a message to a chat - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - chatId - * - message - * properties: - * chatId: - * type: string - * message: - * type: string - * responses: - * 201: - * description: Message sent successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ChatMessage' - * - * /api/chats/archive/{chatId}: - * post: - * tags: [Chats] - * summary: Archive chat - * description: Archive a chat room - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat archived successfully - * 404: - * description: Chat not found - * - * /api/chats/restore/{chatId}: - * post: - * tags: [Chats] - * summary: Restore chat - * description: Restore an archived chat room - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat restored successfully - * 404: - * description: Chat not found - * - * /api/chats/archived/game/{gameId}: - * get: - * tags: [Chats] - * summary: Get archived chats by game - * description: Get all archived chats for a specific game - * security: - * - bearerAuth: [] - * parameters: - * - name: gameId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Archived chats - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - * - * /api/deck-import-export/export/{deckId}: - * get: - * tags: [Import/Export] - * summary: Export deck - * description: Export a deck as JSON or .spr file - * security: - * - bearerAuth: [] - * parameters: - * - name: deckId - * in: path - * required: true - * schema: - * type: string - * - name: format - * in: query - * schema: - * type: string - * enum: [json, spr] - * default: json - * responses: - * 200: - * description: Deck exported successfully - * content: - * application/json: - * schema: - * type: object - * application/octet-stream: - * schema: - * type: string - * format: binary - * - * /api/deck-import-export/import: - * post: - * tags: [Import/Export] - * summary: Import deck - * description: Import a deck from JSON or .spr file - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * properties: - * file: - * type: string - * format: binary - * responses: - * 201: - * description: Deck imported successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * - * /api/admin/users/page/{from}/{to}: - * get: - * tags: [Admin - Users] - * summary: Get users by page (Admin) - * description: Admin endpoint to retrieve paginated list of users - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated users - * content: - * application/json: - * schema: - * type: object - * properties: - * users: - * type: array - * items: - * $ref: '#/components/schemas/User' - * totalCount: - * type: integer - * - * /api/admin/users/{userId}: - * get: - * tags: [Admin - Users] - * summary: Get user by ID (Admin) - * description: Admin endpoint to get specific user details - * security: - * - bearerAuth: [] - * parameters: - * - name: userId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * delete: - * tags: [Admin - Users] - * summary: Delete user (Admin) - * description: Admin endpoint to delete a user - * security: - * - bearerAuth: [] - * parameters: - * - name: userId - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: User deleted successfully - * - * /api/admin/users/search/{searchTerm}: - * get: - * tags: [Admin - Users] - * summary: Search users (Admin) - * description: Admin endpoint to search users - * security: - * - bearerAuth: [] - * parameters: - * - name: searchTerm - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/User' - * - * /api/admin/users/{userId}/deactivate: - * post: - * tags: [Admin - Users] - * summary: Deactivate user (Admin) - * description: Admin endpoint to deactivate a user - * security: - * - bearerAuth: [] - * parameters: - * - name: userId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User deactivated successfully - * - * /api/admin/decks/page/{from}/{to}: - * get: - * tags: [Admin - Decks] - * summary: Get decks by page (Admin) - * description: Admin endpoint to retrieve paginated list of decks - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated decks - * content: - * application/json: - * schema: - * type: object - * properties: - * decks: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - * - * /api/admin/decks/{id}: - * get: - * tags: [Admin - Decks] - * summary: Get deck by ID (Admin) - * description: Admin endpoint to get specific deck details - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * - * /api/admin/decks/search/{searchTerm}: - * get: - * tags: [Admin - Decks] - * summary: Search decks (Admin) - * description: Admin endpoint to search decks - * security: - * - bearerAuth: [] - * parameters: - * - name: searchTerm - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * - * /api/admin/decks/{id}/hard: - * delete: - * tags: [Admin - Decks] - * summary: Hard delete deck (Admin) - * description: Admin endpoint to permanently delete a deck - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Deck permanently deleted - * - * /api/admin/organizations: - * post: - * tags: [Admin - Organizations] - * summary: Create organization (Admin) - * description: Admin endpoint to create a new organization - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * properties: - * name: - * type: string - * description: - * type: string - * responses: - * 201: - * description: Organization created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Organization' - * - * /api/admin/organizations/page/{from}/{to}: - * get: - * tags: [Admin - Organizations] - * summary: Get organizations by page (Admin) - * description: Admin endpoint to retrieve paginated list of organizations - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated organizations - * content: - * application/json: - * schema: - * type: object - * properties: - * organizations: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * totalCount: - * type: integer - * - * /api/admin/organizations/{id}: - * get: - * tags: [Admin - Organizations] - * summary: Get organization by ID (Admin) - * description: Admin endpoint to get specific organization details - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Organization details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Organization' - * delete: - * tags: [Admin - Organizations] - * summary: Delete organization (Admin) - * description: Admin endpoint to soft delete an organization - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Organization deleted successfully - * - * /api/admin/organizations/search/{searchTerm}: - * get: - * tags: [Admin - Organizations] - * summary: Search organizations (Admin) - * description: Admin endpoint to search organizations - * security: - * - bearerAuth: [] - * parameters: - * - name: searchTerm - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * - * /api/admin/organizations/{id}/hard: - * delete: - * tags: [Admin - Organizations] - * summary: Hard delete organization (Admin) - * description: Admin endpoint to permanently delete an organization - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Organization permanently deleted - * - * /api/admin/chats/page/{from}/{to}: - * get: - * tags: [Admin - Chats] - * summary: Get chats by page (Admin) - * description: Admin endpoint to retrieve paginated list of chats - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated chats - * content: - * application/json: - * schema: - * type: object - * properties: - * chats: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - * totalCount: - * type: integer - * - * /api/admin/chats/{id}: - * get: - * tags: [Admin - Chats] - * summary: Get chat by ID (Admin) - * description: Admin endpoint to get specific chat details - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Chat' - * - * /api/admin/contacts/page/{from}/{to}: - * get: - * tags: [Admin - Contacts] - * summary: Get contacts by page (Admin) - * description: Admin endpoint to retrieve paginated list of contacts - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated contacts - * content: - * application/json: - * schema: - * type: object - * properties: - * contacts: - * type: array - * items: - * $ref: '#/components/schemas/Contact' - * totalCount: - * type: integer - * - * /api/admin/contacts/{id}: - * get: - * tags: [Admin - Contacts] - * summary: Get contact by ID (Admin) - * description: Admin endpoint to get specific contact details - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Contact details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - * - * /api/admin/contacts/search/{searchTerm}: - * get: - * tags: [Admin - Contacts] - * summary: Search contacts (Admin) - * description: Admin endpoint to search contacts - * security: - * - bearerAuth: [] - * parameters: - * - name: searchTerm - * in: path - * required: true - * schema: - * type: string - * - name: includeDeleted - * in: query - * required: true - * schema: - * type: boolean - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Contact' - * - * /api/contacts: - * post: - * tags: [Contacts] - * summary: Create contact - * description: Create a new contact message - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateContactRequest' - * responses: - * 201: - * description: Contact created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - * - * /api/games/start: - * post: - * summary: Start a new game - * tags: [Games] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - deckids - * - maxplayers - * - logintype - * properties: - * deckids: - * type: array - * items: - * type: string - * description: Array of deck IDs (must include all 3 types LUCK, JOKER, QUESTION) - * maxplayers: - * type: integer - * minimum: 2 - * maximum: 8 - * description: Maximum number of players allowed in the game - * logintype: - * type: integer - * enum: [0, 1, 2] - * description: How players can join (PUBLIC=0, PRIVATE=1, ORGANIZATION=2) - * responses: - * 200: - * description: Game started successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * id: - * type: string - * description: Game UUID - * gamecode: - * type: string - * description: 6-character game code for joining - * maxplayers: - * type: integer - * logintype: - * type: integer - * gamedecks: - * type: array - * description: Shuffled game decks - * players: - * type: array - * items: - * type: string - * started: - * type: boolean - * finished: - * type: boolean - * state: - * type: integer - * description: Game state (WAITING=0, ACTIVE=1, FINISHED=2, CANCELLED=3) - * createdate: - * type: string - * format: date-time - * 400: - * description: Invalid input parameters - * 401: - * description: Authentication required - * 500: - * description: Internal server error - * - * /api/games/join: - * post: - * summary: Join a game (automatically detects game type) - * description: Join any game by providing the game code. The system automatically determines if authentication is required based on the game type. - * tags: [Games] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - gameCode - * properties: - * gameCode: - * type: string - * description: 6-character game code - * example: "ABC123" - * playerName: - * type: string - * description: Display name for the player (required for public games, optional for authenticated games) - * example: "John Doe" - * responses: - * 200: - * description: Successfully joined the game - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - * 400: - * description: Invalid input or missing required fields - * 401: - * description: Authentication required for this game type - * 403: - * description: Organization membership required - * 404: - * description: Game not found - * 409: - * description: Game is full or not accepting players - * 500: - * description: Internal server error - * - * /api/games/{gameId}/start: - * post: - * summary: Start gameplay for an existing game - * description: Initialize gameplay by setting all player positions to 0 and assigning random turn order. This is separate from game creation. - * tags: [Games] - * parameters: - * - in: path - * name: gameId - * required: true - * schema: - * type: string - * description: The ID of the game to start - * responses: - * 200: - * description: Game started successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Game started successfully" - * gameId: - * type: string - * example: "game123" - * playerCount: - * type: number - * example: 4 - * 400: - * description: Invalid input or game cannot be started - * 401: - * description: Authentication required - * 403: - * description: Only game master can start the game - * 404: - * description: Game not found - * 409: - * description: Game already started or not ready to start - * 500: - * description: Internal server error -<<<<<<< HEAD -======= ->>>>>>> origin/main -======= ->>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2 - */ - -export {}; diff --git a/SerpentRace_Backend/src/Api/swagger/swaggerDefinitionsFixed.ts b/SerpentRace_Backend/src/Api/swagger/swaggerDefinitionsFixed.ts deleted file mode 100644 index 1f019805..00000000 --- a/SerpentRace_Backend/src/Api/swagger/swaggerDefinitionsFixed.ts +++ /dev/null @@ -1,2788 +0,0 @@ -/** - * @swagger - * components: - * schemas: - * User: - * type: object - * properties: - * id: - * type: string - * format: uuid - * username: - * type: string - * email: - * type: string - * format: email - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * nullable: true - * type: - * type: string - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * orgid: - * type: string - * nullable: true - * - * CreateUserRequest: - * type: object - * required: - * - username - * - email - * - password - * - fname - * - lname - * - type - * properties: - * username: - * type: string - * email: - * type: string - * format: email - * password: - * type: string - * format: password - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * type: - * type: string - * - * LoginRequest: - * type: object - * required: - * - username - * - password - * properties: - * username: - * type: string - * password: - * type: string - * format: password - * - * LoginResponse: - * type: object - * properties: - * token: - * type: string - * user: - * $ref: '#/components/schemas/User' - * requiresOrgReauth: - * type: boolean - * orgLoginUrl: - * type: string - * organizationName: - * type: string - * - * UpdateProfileRequest: - * type: object - * properties: - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * email: - * type: string - * format: email - * - * ForgotPasswordRequest: - * type: object - * required: - * - email - * properties: - * email: - * type: string - * format: email - * - * ResetPasswordRequest: - * type: object - * required: - * - token - * - newPassword - * properties: - * token: - * type: string - * newPassword: - * type: string - * format: password - * minLength: 6 - * maxLength: 100 - * - * AuthSuccessResponse: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * - * Organization: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * contactfname: - * type: string - * contactlname: - * type: string - * contactphone: - * type: string - * contactemail: - * type: string - * format: email - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * url: - * type: string - * nullable: true - * userinorg: - * type: integer - * maxOrganizationalDecks: - * type: integer - * - * Deck: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * enum: [0, 1, 2, 3] - * description: 0=JOKER, 1=LUCK, 2=QUESTION, 3=GENERAL - * userid: - * type: string - * format: uuid - * creationdate: - * type: string - * format: date-time - * cards: - * type: array - * items: - * type: object - * playedNumber: - * type: integer - * ctype: - * type: integer - * enum: [0, 1, 2] - * description: 0=PUBLIC, 1=ORGANIZATIONAL, 2=PRIVATE - * updatedate: - * type: string - * format: date-time - * state: - * type: integer - * enum: [0, 1, 2] - * description: 0=ACTIVE, 1=INACTIVE, 2=SOFT_DELETE - * organization: - * $ref: '#/components/schemas/Organization' - * nullable: true - * - * CreateDeckRequest: - * type: object - * required: - * - name - * - type - * - cards - * properties: - * name: - * type: string - * type: - * type: integer - * cards: - * type: array - * items: - * type: object - * ctype: - * type: integer - * - * Chat: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * participants: - * type: array - * items: - * type: string - * creatorId: - * type: string - * gameId: - * type: string - * nullable: true - * createDate: - * type: string - * format: date-time - * updateDate: - * type: string - * format: date-time - * state: - * type: integer - * - * ChatMessage: - * type: object - * properties: - * id: - * type: string - * format: uuid - * senderId: - * type: string - * senderName: - * type: string - * message: - * type: string - * timestamp: - * type: string - * format: date-time - * chatId: - * type: string - * - * Contact: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * email: - * type: string - * format: email - * userid: - * type: string - * format: uuid - * nullable: true - * type: - * type: integer - * enum: [0, 1, 2] - * description: 0=QUESTION, 1=BUG_REPORT, 2=SUGGESTION - * txt: - * type: string - * state: - * type: integer - * createDate: - * type: string - * format: date-time - * updateDate: - * type: string - * format: date-time - * adminResponse: - * type: string - * nullable: true - * responseDate: - * type: string - * format: date-time - * nullable: true - * respondedBy: - * type: string - * nullable: true - * - * CreateContactRequest: - * type: object - * required: - * - name - * - email - * - type - * - txt - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * type: - * type: integer - * txt: - * type: string - * - * Game: - * type: object - * properties: - * id: - * type: string - * format: uuid - * gamecode: - * type: string - * maxplayers: - * type: integer - * logintype: - * type: integer - * gamedecks: - * type: array - * players: - * type: array - * items: - * type: string - * started: - * type: boolean - * finished: - * type: boolean - * state: - * type: integer - * createdate: - * type: string - * format: date-time - * - * Error: - * type: object - * properties: - * error: - * type: string - * timestamp: - * type: string - * format: date-time - * details: - * type: string - */ - -/** - * @swagger - * /api/users/login: - * post: - * tags: [Users] - * summary: User login - * description: Authenticate user and return JWT token - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginRequest' - * responses: - * 200: - * description: Login successful - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/LoginResponse' - * 401: - * description: Invalid credentials - */ - -/** - * @swagger - * /api/users/create: - * post: - * tags: [Users] - * summary: Create new user - * description: Register a new user account - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateUserRequest' - * responses: - * 201: - * description: User created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 400: - * description: Validation error - * 409: - * description: User already exists - */ - -/** - * @swagger - * /api/users/profile: - * get: - * tags: [Users] - * summary: Get user profile - * description: Get current user's profile information - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: User profile data - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 401: - * description: Unauthorized - * patch: - * tags: [Users] - * summary: Update user profile - * description: Update current user's profile information - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/UpdateProfileRequest' - * responses: - * 200: - * description: Profile updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 400: - * description: Validation error - * 401: - * description: Unauthorized - */ - -/** - * @swagger - * /api/users/verify-email/{token}: - * get: - * tags: [Users] - * summary: Verify email address - * description: Verify user's email address using verification token - * parameters: - * - name: token - * in: path - * required: true - * schema: - * type: string - * description: Email verification token - * responses: - * 200: - * description: Email verified successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AuthSuccessResponse' - * 400: - * description: Invalid or expired verification token - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/users/forgot-password: - * post: - * tags: [Users] - * summary: Request password reset - * description: Send password reset email to user - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ForgotPasswordRequest' - * responses: - * 200: - * description: Password reset email sent (if email exists) - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AuthSuccessResponse' - * 400: - * description: Validation error - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/users/reset-password: - * post: - * tags: [Users] - * summary: Reset password - * description: Reset user password using reset token - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ResetPasswordRequest' - * responses: - * 200: - * description: Password reset successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/AuthSuccessResponse' - * 400: - * description: Invalid token or password validation failed - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/organizations/search: - * get: - * tags: [Organizations] - * summary: Search organizations - * description: Search organizations by query - * security: - * - bearerAuth: [] - * parameters: - * - name: query - * in: query - * required: true - * schema: - * type: string - * - name: from - * in: query - * required: true - * schema: - * type: integer - * - name: to - * in: query - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: object - * properties: - * results: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * totalCount: - * type: integer - */ - -/** - * @swagger - * /api/organizations/{orgId}/login-url: - * get: - * tags: [Organizations] - * summary: Get organization login URL - * description: Get OAuth login URL for organization - * security: - * - bearerAuth: [] - * parameters: - * - name: orgId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Login URL - * content: - * application/json: - * schema: - * type: object - * properties: - * loginUrl: - * type: string - */ - -/** - * @swagger - * /api/organizations/auth-callback: - * post: - * tags: [Organizations] - * summary: OAuth callback - * description: Handle OAuth callback from organization - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * code: - * type: string - * state: - * type: string - * responses: - * 200: - * description: Authentication successful - * 400: - * description: Invalid callback data - */ - -/** - * @swagger - * /api/decks/page/{from}/{to}: - * get: - * tags: [Decks] - * summary: Get decks by page - * description: Retrieve paginated list of decks - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated decks - * content: - * application/json: - * schema: - * type: object - * properties: - * decks: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - */ - -/** - * @swagger - * /api/decks/search: - * get: - * tags: [Decks] - * summary: Search decks - * description: Search decks by query - * security: - * - bearerAuth: [] - * parameters: - * - name: query - * in: query - * required: true - * schema: - * type: string - * - name: from - * in: query - * required: true - * schema: - * type: integer - * - name: to - * in: query - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: object - * properties: - * results: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * totalCount: - * type: integer - */ - -/** - * @swagger - * /api/decks/{id}: - * get: - * tags: [Decks] - * summary: Get deck by ID - * description: Retrieve a specific deck by ID - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck details - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * 404: - * description: Deck not found - * put: - * tags: [Decks] - * summary: Update deck - * description: Update an existing deck - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CreateDeckRequest' - * responses: - * 200: - * description: Deck updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * delete: - * tags: [Decks] - * summary: Delete deck - * description: Delete a deck (soft delete) - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 204: - * description: Deck deleted successfully - * 404: - * description: Deck not found - */ - -/** - * @swagger - * /api/chats/user-chats: - * get: - * tags: [Chats] - * summary: Get user chats - * description: Get all chats for the current user - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: User chats - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - */ - -/** - * @swagger - * /api/chats/history/{chatId}: - * get: - * tags: [Chats] - * summary: Get chat history - * description: Get message history for a chat - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * - name: page - * in: query - * schema: - * type: integer - * - name: limit - * in: query - * schema: - * type: integer - * responses: - * 200: - * description: Chat history - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - */ - -/** - * @swagger - * /api/chats/message: - * post: - * tags: [Chats] - * summary: Send message - * description: Send a message to a chat - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - chatId - * - message - * properties: - * chatId: - * type: string - * message: - * type: string - * responses: - * 201: - * description: Message sent successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ChatMessage' - */ - -/** - * @swagger - * /api/chats/archive/{chatId}: - * post: - * tags: [Chats] - * summary: Archive chat - * description: Archive a chat room - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat archived successfully - * 404: - * description: Chat not found - */ - -/** - * @swagger - * /api/chats/restore/{chatId}: - * post: - * tags: [Chats] - * summary: Restore chat - * description: Restore an archived chat room - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat restored successfully - * 404: - * description: Chat not found - */ - -/** - * @swagger - * /api/contact/create: - * post: - * tags: [Contact] - * summary: Create contact message - * description: Send a contact message to the system - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - email - * - message - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * subject: - * type: string - * nullable: true - * message: - * type: string - * responses: - * 201: - * description: Contact message sent successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - */ - -/** - * @swagger - * /api/admin/users: - * get: - * tags: [Admin - Users] - * summary: Get all users (Admin) - * description: Retrieve all users in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: search - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Users retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * users: - * type: array - * items: - * $ref: '#/components/schemas/User' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/users/{id}: - * get: - * tags: [Admin - Users] - * summary: Get user by ID (Admin) - * description: Get detailed information about a specific user - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * 404: - * description: User not found - * put: - * tags: [Admin - Users] - * summary: Update user (Admin) - * description: Update user information by admin - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * username: - * type: string - * email: - * type: string - * format: email - * firstName: - * type: string - * lastName: - * type: string - * isActive: - * type: boolean - * role: - * type: string - * enum: [user, admin, moderator] - * responses: - * 200: - * description: User updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - * delete: - * tags: [Admin - Users] - * summary: Delete user (Admin) - * description: Delete a user from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User deleted successfully - * 404: - * description: User not found - */ - -/** - * @swagger - * /api/admin/users/{id}/ban: - * post: - * tags: [Admin - Users] - * summary: Ban user (Admin) - * description: Ban a user from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * content: - * application/json: - * schema: - * type: object - * properties: - * reason: - * type: string - * duration: - * type: string - * description: Ban duration in ISO format or 'permanent' - * responses: - * 200: - * description: User banned successfully - */ - -/** - * @swagger - * /api/admin/users/{id}/unban: - * post: - * tags: [Admin - Users] - * summary: Unban user (Admin) - * description: Remove ban from a user - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: User unbanned successfully - */ - -/** - * @swagger - * /api/admin/decks: - * get: - * tags: [Admin - Decks] - * summary: Get all decks (Admin) - * description: Retrieve all decks in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: search - * in: query - * schema: - * type: string - * - name: status - * in: query - * schema: - * type: string - * enum: [public, private, reported] - * responses: - * 200: - * description: Decks retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * decks: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/decks/{id}: - * get: - * tags: [Admin - Decks] - * summary: Get deck by ID (Admin) - * description: Get detailed information about a specific deck - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * 404: - * description: Deck not found - * put: - * tags: [Admin - Decks] - * summary: Update deck (Admin) - * description: Update deck information by admin - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * description: - * type: string - * isPublic: - * type: boolean - * isActive: - * type: boolean - * responses: - * 200: - * description: Deck updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * delete: - * tags: [Admin - Decks] - * summary: Delete deck (Admin) - * description: Delete a deck from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck deleted successfully - * 404: - * description: Deck not found - */ - -/** - * @swagger - * /api/admin/decks/{id}/moderate: - * post: - * tags: [Admin - Decks] - * summary: Moderate deck (Admin) - * description: Approve or reject a deck for public visibility - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - action - * properties: - * action: - * type: string - * enum: [approve, reject, flag] - * reason: - * type: string - * responses: - * 200: - * description: Deck moderated successfully - */ - -/** - * @swagger - * /api/admin/organizations: - * get: - * tags: [Admin - Organizations] - * summary: Get all organizations (Admin) - * description: Retrieve all organizations in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: search - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Organizations retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * organizations: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/organizations/{id}: - * get: - * tags: [Admin - Organizations] - * summary: Get organization by ID (Admin) - * description: Get detailed information about a specific organization - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Organization found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Organization' - * 404: - * description: Organization not found - * put: - * tags: [Admin - Organizations] - * summary: Update organization (Admin) - * description: Update organization information by admin - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * name: - * type: string - * description: - * type: string - * isActive: - * type: boolean - * responses: - * 200: - * description: Organization updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Organization' - * delete: - * tags: [Admin - Organizations] - * summary: Delete organization (Admin) - * description: Delete an organization from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Organization deleted successfully - * 404: - * description: Organization not found - */ - -/** - * @swagger - * /api/admin/organizations/{id}/members: - * get: - * tags: [Admin - Organizations] - * summary: Get organization members (Admin) - * description: Get all members of an organization - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Organization members - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/User' - */ - -/** - * @swagger - * /api/admin/chats: - * get: - * tags: [Admin - Chats] - * summary: Get all chats (Admin) - * description: Retrieve all chats in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: search - * in: query - * schema: - * type: string - * - name: status - * in: query - * schema: - * type: string - * enum: [active, archived, reported] - * responses: - * 200: - * description: Chats retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * chats: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/chats/{id}: - * get: - * tags: [Admin - Chats] - * summary: Get chat by ID (Admin) - * description: Get detailed information about a specific chat - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Chat' - * 404: - * description: Chat not found - * delete: - * tags: [Admin - Chats] - * summary: Delete chat (Admin) - * description: Delete a chat from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat deleted successfully - * 404: - * description: Chat not found - */ - -/** - * @swagger - * /api/admin/chats/{id}/messages: - * get: - * tags: [Admin - Chats] - * summary: Get chat messages (Admin) - * description: Get all messages in a chat - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * - name: page - * in: query - * schema: - * type: integer - * - name: limit - * in: query - * schema: - * type: integer - * responses: - * 200: - * description: Chat messages - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - */ - -/** - * @swagger - * /api/admin/chats/{id}/moderate: - * post: - * tags: [Admin - Chats] - * summary: Moderate chat (Admin) - * description: Moderate a chat room (archive, restore, flag) - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - action - * properties: - * action: - * type: string - * enum: [archive, restore, flag, unflag] - * reason: - * type: string - * responses: - * 200: - * description: Chat moderated successfully - */ - -/** - * @swagger - * /api/admin/contacts: - * get: - * tags: [Admin - Contacts] - * summary: Get all contact messages (Admin) - * description: Retrieve all contact messages with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: status - * in: query - * schema: - * type: string - * enum: [unread, read, resolved] - * - name: search - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Contact messages retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * contacts: - * type: array - * items: - * $ref: '#/components/schemas/Contact' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/contacts/{id}: - * get: - * tags: [Admin - Contacts] - * summary: Get contact message by ID (Admin) - * description: Get detailed information about a specific contact message - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Contact message found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - * 404: - * description: Contact message not found - * put: - * tags: [Admin - Contacts] - * summary: Update contact message status (Admin) - * description: Update the status of a contact message - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * enum: [unread, read, resolved] - * adminNotes: - * type: string - * responses: - * 200: - * description: Contact message updated successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - * delete: - * tags: [Admin - Contacts] - * summary: Delete contact message (Admin) - * description: Delete a contact message from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Contact message deleted successfully - * 404: - * description: Contact message not found - */ - -/** - * @swagger - * /api/admin/contacts/{id}/reply: - * post: - * tags: [Admin - Contacts] - * summary: Reply to contact message (Admin) - * description: Send a reply to a contact message - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - reply - * properties: - * reply: - * type: string - * responses: - * 200: - * description: Reply sent successfully - */ - -/** - * @swagger - * /api/admin/games: - * get: - * tags: [Admin - Games] - * summary: Get all games (Admin) - * description: Retrieve all games in the system with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: page - * in: query - * schema: - * type: integer - * default: 1 - * - name: limit - * in: query - * schema: - * type: integer - * default: 10 - * - name: status - * in: query - * schema: - * type: string - * enum: [active, completed, abandoned] - * responses: - * 200: - * description: Games retrieved successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * games: - * type: array - * items: - * $ref: '#/components/schemas/Game' - * total: - * type: integer - * page: - * type: integer - * limit: - * type: integer - */ - -/** - * @swagger - * /api/admin/games/{id}: - * get: - * tags: [Admin - Games] - * summary: Get game by ID (Admin) - * description: Get detailed information about a specific game - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Game found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - * 404: - * description: Game not found - * delete: - * tags: [Admin - Games] - * summary: Delete game (Admin) - * description: Delete a game from the system - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Game deleted successfully - * 404: - * description: Game not found - */ - -/** - * @swagger - * /api/admin/system/stats: - * get: - * tags: [Admin - System] - * summary: Get system statistics (Admin) - * description: Get comprehensive system statistics - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: System statistics - * content: - * application/json: - * schema: - * type: object - * properties: - * uptime: - * type: string - * version: - * type: string - * memoryUsage: - * type: object - * activeConnections: - * type: integer - * databaseHealth: - * type: string - * redisHealth: - * type: string - */ - -/** - * @swagger - * /api/admin/system/logs: - * get: - * tags: [Admin - System] - * summary: Get system logs (Admin) - * description: Retrieve system logs with filtering - * security: - * - bearerAuth: [] - * parameters: - * - name: level - * in: query - * schema: - * type: string - * enum: [error, warn, info, debug] - * - name: limit - * in: query - * schema: - * type: integer - * default: 100 - * - name: since - * in: query - * schema: - * type: string - * format: date-time - * responses: - * 200: - * description: System logs - * content: - * application/json: - * schema: - * type: array - * items: - * type: object - * properties: - * timestamp: - * type: string - * level: - * type: string - * message: - * type: string - * metadata: - * type: object - */ - -/** - * @swagger - * /api/health: - * get: - * tags: [System] - * summary: Health check - * description: Check the health status of the API - * responses: - * 200: - * description: Service is healthy - * content: - * application/json: - * schema: - * type: object - * properties: - * status: - * type: string - * example: "OK" - * timestamp: - * type: string - * format: date-time - * version: - * type: string - */ - -/** - * @swagger - * components: - * securitySchemes: - * bearerAuth: - * type: http - * scheme: bearer - * schemas: - * User: - * type: object - * properties: - * id: - * type: string - * format: uuid - * username: - * type: string - * email: - * type: string - * format: email - * fname: - * type: string - * lname: - * type: string - * phone: - * type: string - * nullable: true - * type: - * type: string - * state: - * type: integer - * regdate: - * type: string - * format: date-time - * updatedate: - * type: string - * format: date-time - * orgid: - * type: string - * nullable: true - * Deck: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * type: - * type: integer - * userid: - * type: string - * format: uuid - * creationdate: - * type: string - * format: date-time - * cards: - * type: array - * items: - * type: object - * Organization: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * description: - * type: string - * members: - * type: array - * items: - * $ref: '#/components/schemas/User' - * Contact: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * email: - * type: string - * format: email - * type: - * type: integer - * txt: - * type: string - * userid: - * type: string - * nullable: true - * createdate: - * type: string - * format: date-time - * Chat: - * type: object - * properties: - * id: - * type: string - * format: uuid - * name: - * type: string - * gameId: - * type: string - * format: uuid - * messages: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - * ChatMessage: - * type: object - * properties: - * id: - * type: string - * format: uuid - * chatId: - * type: string - * format: uuid - * senderId: - * type: string - * format: uuid - * message: - * type: string - * timestamp: - * type: string - * format: date-time - * Game: - * type: object - * properties: - * id: - * type: string - * format: uuid - * deckids: - * type: array - * items: - * type: string - * maxplayers: - * type: integer - * logintype: - * type: string - * state: - * type: string - * createdate: - * type: string - * format: date-time - */ - -/** - * @swagger - * tags: - * - name: Users - * - name: Decks - * - name: Organizations - * - name: Contact - * - name: Chats - * - name: Games - * - name: Admin - Users - * - name: Admin - Decks - * - name: Admin - Organizations - * - name: Admin - Contacts - * - name: Admin - Chats - * - name: Admin - Games - * - name: System - */ - -// User endpoints -/** - * @swagger - * /api/users/login: - * post: - * tags: [Users] - * summary: User login - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [username, password] - * properties: - * username: - * type: string - * password: - * type: string - * responses: - * 200: - * description: Login successful - * content: - * application/json: - * schema: - * type: object - * properties: - * token: - * type: string - */ - -/** - * @swagger - * /api/users/create: - * post: - * tags: [Users] - * summary: Create user - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [username, email, password] - * properties: - * username: - * type: string - * email: - * type: string - * password: - * type: string - * responses: - * 201: - * description: User created - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/User' - */ - -// ...existing code... -/** - * @swagger - * /api/decks/page/{from}/{to}: - * get: - * tags: [Decks] - * summary: Get decks with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated decks - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - */ - -/** - * @swagger - * /api/decks/search: - * get: - * tags: [Decks] - * summary: Search decks - * security: - * - bearerAuth: [] - * parameters: - * - name: q - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - */ - -/** - * @swagger - * /api/decks/{id}: - * get: - * tags: [Decks] - * summary: Get deck by ID - * security: - * - bearerAuth: [] - * parameters: - * - name: id - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Deck found - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - * 404: - * description: Deck not found - */ - -/** - * @swagger - * /api/organizations/page/{from}/{to}: - * get: - * tags: [Organizations] - * summary: Get organizations with pagination - * security: - * - bearerAuth: [] - * parameters: - * - name: from - * in: path - * required: true - * schema: - * type: integer - * - name: to - * in: path - * required: true - * schema: - * type: integer - * responses: - * 200: - * description: Paginated organizations - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - */ - -/** - * @swagger - * /api/organizations/search: - * get: - * tags: [Organizations] - * summary: Search organizations - * security: - * - bearerAuth: [] - * parameters: - * - name: q - * in: query - * schema: - * type: string - * responses: - * 200: - * description: Search results - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - */ - -/** - * @swagger - * /api/contact/create: - * post: - * tags: [Contact] - * summary: Create contact message - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [name, email, txt] - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * txt: - * type: string - * responses: - * 201: - * description: Contact created - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - */ - -/** - * @swagger - * /api/chats/create: - * post: - * tags: [Chats] - * summary: Create chat - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [name, gameId] - * properties: - * name: - * type: string - * gameId: - * type: string - * responses: - * 201: - * description: Chat created - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Chat' - */ - -/** - * @swagger - * /api/chats/history/{chatId}: - * get: - * tags: [Chats] - * summary: Get chat history - * security: - * - bearerAuth: [] - * parameters: - * - name: chatId - * in: path - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Chat history - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/ChatMessage' - */ - -/** - * @swagger - * /api/games/start: - * post: - * tags: [Games] - * summary: Start a new game - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [deckids, maxplayers, logintype] - * properties: - * deckids: - * type: array - * items: - * type: string - * maxplayers: - * type: integer - * logintype: - * type: string - * responses: - * 201: - * description: Game started - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - */ - -/** - * @swagger - * /api/games/join: - * post: - * tags: [Games] - * summary: Join a game - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: [gameId] - * properties: - * gameId: - * type: string - * responses: - * 200: - * description: Joined game - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - */ - -// Admin endpoints (examples) -/** - * @swagger - * /api/admin/users: - * get: - * tags: [Admin - Users] - * summary: Get all users (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Users retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/User' - */ - -/** - * @swagger - * /api/admin/decks: - * get: - * tags: [Admin - Decks] - * summary: Get all decks (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Decks retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Deck' - */ - -/** - * @swagger - * /api/admin/organizations: - * get: - * tags: [Admin - Organizations] - * summary: Get all organizations (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Organizations retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Organization' - */ - -/** - * @swagger - * /api/admin/contacts: - * get: - * tags: [Admin - Contacts] - * summary: Get all contact messages (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Contacts retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Contact' - */ - -/** - * @swagger - * /api/admin/chats: - * get: - * tags: [Admin - Chats] - * summary: Get all chats (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Chats retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Chat' - */ - -/** - * @swagger - * /api/admin/games: - * get: - * tags: [Admin - Games] - * summary: Get all games (Admin) - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: Games retrieved - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: '#/components/schemas/Game' - */ - -/** - * @swagger - * /api/contacts: - * post: - * tags: [Contacts] - * summary: Create contact - * description: Create a new contact message - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - name - * - email - * - type - * - txt - * properties: - * name: - * type: string - * email: - * type: string - * format: email - * type: - * type: integer - * enum: [0, 1, 2] - * description: 0=QUESTION, 1=BUG_REPORT, 2=SUGGESTION - * txt: - * type: string - * responses: - * 201: - * description: Contact created successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Contact' - */ - -/** - * @swagger - * /api/deck-import-export/export/{deckId}: - * get: - * tags: [Deck Import/Export] - * summary: Export deck - * description: Export a deck as JSON or .spr file - * security: - * - bearerAuth: [] - * parameters: - * - name: deckId - * in: path - * required: true - * schema: - * type: string - * - name: format - * in: query - * schema: - * type: string - * enum: [json, spr] - * default: json - * responses: - * 200: - * description: Deck exported successfully - * content: - * application/json: - * schema: - * type: object - * application/octet-stream: - * schema: - * type: string - * format: binary - */ - -/** - * @swagger - * /api/deck-import-export/import: - * post: - * tags: [Deck Import/Export] - * summary: Import deck - * description: Import a deck from JSON or .spr file - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * multipart/form-data: - * schema: - * type: object - * properties: - * file: - * type: string - * format: binary - * responses: - * 201: - * description: Deck imported successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Deck' - */ - -/** - * @swagger - * /api/games/start: - * post: - * summary: Start a new game - * tags: [Games] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - deckids - * - maxplayers - * - logintype - * properties: - * deckids: - * type: array - * items: - * type: string - * description: Array of deck IDs (must include all 3 types LUCK, JOKER, QUESTION) - * maxplayers: - * type: integer - * minimum: 2 - * maximum: 8 - * description: Maximum number of players allowed in the game - * logintype: - * type: integer - * enum: [0, 1, 2] - * description: How players can join (PUBLIC=0, PRIVATE=1, ORGANIZATION=2) - * responses: - * 200: - * description: Game started successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - * 400: - * description: Invalid input parameters - * 401: - * description: Authentication required - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/games/join: - * post: - * summary: Join a game (automatically detects game type) - * description: Join any game by providing the game code. The system automatically determines if authentication is required based on the game type. - * tags: [Games] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - gameCode - * properties: - * gameCode: - * type: string - * description: 6-character game code - * example: "ABC123" - * playerName: - * type: string - * description: Display name for the player (required for public games, optional for authenticated games) - * example: "John Doe" - * responses: - * 200: - * description: Successfully joined the game - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/Game' - * 400: - * description: Invalid input or missing required fields - * 401: - * description: Authentication required for this game type - * 403: - * description: Organization membership required - * 404: - * description: Game not found - * 409: - * description: Game is full or not accepting players - * 500: - * description: Internal server error - */ - -/** - * @swagger - * /api/games/{gameId}/start: - * post: - * summary: Start gameplay for an existing game - * description: Initialize gameplay by setting all player positions to 0 and assigning random turn order. This is separate from game creation. - * tags: [Games] - * parameters: - * - in: path - * name: gameId - * required: true - * schema: - * type: string - * description: The ID of the game to start - * responses: - * 200: - * description: Game started successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * message: - * type: string - * example: "Game started successfully" - * gameId: - * type: string - * example: "game123" - * playerCount: - * type: number - * example: 4 - * 400: - * description: Invalid input or game cannot be started - * 401: - * description: Authentication required - * 403: - * description: Only game master can start the game - * 404: - * description: Game not found - * 409: - * description: Game already started or not ready to start - * 500: - * description: Internal server error - */ - -export {}; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Api/swagger/swaggerUiSetup.ts b/SerpentRace_Backend/src/Api/swagger/swaggerUiSetup.ts deleted file mode 100644 index f474fe87..00000000 --- a/SerpentRace_Backend/src/Api/swagger/swaggerUiSetup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import express from 'express'; -import swaggerUi from 'swagger-ui-express'; -import { swaggerSpec } from './swaggerConfig'; - -export function setupSwagger(app: express.Application) { - app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/ChatArchiveCommandHandlers.ts b/SerpentRace_Backend/src/Application/Chat/commands/ChatArchiveCommandHandlers.ts deleted file mode 100644 index c9456564..00000000 --- a/SerpentRace_Backend/src/Application/Chat/commands/ChatArchiveCommandHandlers.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ArchiveChatCommand, RestoreChatCommand } from './ChatCommands'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { ChatType } from '../../../Domain/Chat/ChatAggregate'; -import { logAuth, logError, logWarning } from '../../Services/Logger'; - -export class ArchiveChatCommandHandler { - constructor(private chatRepository: IChatRepository) {} - - async execute(command: ArchiveChatCommand): Promise { - try { - const chat = await this.chatRepository.findById(command.chatId); - if (!chat) { - throw new Error('Chat not found'); - } - - await this.chatRepository.archiveChat(chat); - - logAuth('Chat archived manually', undefined, { - chatId: command.chatId, - chatType: chat.type, - messageCount: chat.messages.length - }); - - return true; - - } catch (error) { - logError('ArchiveChatCommandHandler error', error as Error); - return false; - } - } -} - -export class RestoreChatCommandHandler { - constructor(private chatRepository: IChatRepository) {} - - async execute(command: RestoreChatCommand): Promise { - try { - const archive = await this.chatRepository.getArchivedChat(command.chatId); - if (!archive) { - throw new Error('Archived chat not found'); - } - - // Game chats cannot be restored, only viewed - if (archive.chatType === ChatType.GAME) { - logWarning('Attempt to restore game chat blocked', { - chatId: command.chatId, - chatType: archive.chatType - }); - return false; - } - - const restoredChat = await this.chatRepository.restoreFromArchive(command.chatId); - if (!restoredChat) { - throw new Error('Failed to restore chat from archive'); - } - - logAuth('Chat restored from archive', undefined, { - chatId: command.chatId, - messageCount: archive.archivedMessages.length - }); - - return true; - - } catch (error) { - logError('RestoreChatCommandHandler error', error as Error); - return false; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/ChatCommands.ts b/SerpentRace_Backend/src/Application/Chat/commands/ChatCommands.ts deleted file mode 100644 index 41d2bb47..00000000 --- a/SerpentRace_Backend/src/Application/Chat/commands/ChatCommands.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface CreateChatCommand { - type: 'direct' | 'group' | 'game'; - name?: string; - gameId?: string; - createdBy: string; - userIds: string[]; -} - -export interface SendMessageCommand { - chatId: string; - userId: string; - message: string; -} - -export interface ArchiveChatCommand { - chatId: string; -} - -export interface RestoreChatCommand { - chatId: string; -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/CreateChatCommandHandler.ts b/SerpentRace_Backend/src/Application/Chat/commands/CreateChatCommandHandler.ts deleted file mode 100644 index d007c28b..00000000 --- a/SerpentRace_Backend/src/Application/Chat/commands/CreateChatCommandHandler.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CreateChatCommand } from './ChatCommands'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { ChatType, ChatAggregate } from '../../../Domain/Chat/ChatAggregate'; -import { UserState } from '../../../Domain/User/UserAggregate'; -import { logAuth, logError } from '../../Services/Logger'; - -export class CreateChatCommandHandler { - constructor( - private chatRepository: IChatRepository, - private userRepository: IUserRepository - ) {} - - async execute(command: CreateChatCommand): Promise { - try { - // Validate creator exists - const creator = await this.userRepository.findById(command.createdBy); - if (!creator) { - throw new Error('Creator not found'); - } - - // For group chats, check if creator is premium - if (command.type === 'group' && creator.state !== UserState.VERIFIED_PREMIUM) { - throw new Error('Premium subscription required to create groups'); - } - - // Validate all target users exist - const targetUsers = await Promise.all( - command.userIds.map(id => this.userRepository.findById(id)) - ); - - if (targetUsers.some(user => !user)) { - throw new Error('One or more target users not found'); - } - - // For direct chats, check if already exists - if (command.type === 'direct' && command.userIds.length === 1) { - const existingChats = await this.chatRepository.findByUserId(command.createdBy); - const existingDirectChat = existingChats.find(chat => - chat.type === ChatType.DIRECT && - chat.users.length === 2 && - chat.users.includes(command.userIds[0]) - ); - - if (existingDirectChat) { - return existingDirectChat; - } - } - - // For game chats, check if already exists - if (command.type === 'game' && command.gameId) { - const existingGameChat = await this.chatRepository.findByGameId(command.gameId); - if (existingGameChat) { - return existingGameChat; - } - } - - // Create chat - const chatData: Partial = { - type: command.type as any, - name: command.name, - gameId: command.gameId, - createdBy: command.createdBy, - users: [command.createdBy, ...command.userIds], - messages: [], - lastActivity: new Date() - }; - - const chat = await this.chatRepository.create(chatData); - - logAuth('Chat created successfully', command.createdBy, { - chatId: chat.id, - chatType: command.type, - participantCount: chat.users.length, - gameId: command.gameId - }); - - return chat; - - } catch (error) { - logError('CreateChatCommandHandler error', error as Error); - return null; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/SendMessageCommandHandler.ts b/SerpentRace_Backend/src/Application/Chat/commands/SendMessageCommandHandler.ts deleted file mode 100644 index 8bd6903a..00000000 --- a/SerpentRace_Backend/src/Application/Chat/commands/SendMessageCommandHandler.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { SendMessageCommand } from './ChatCommands'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { Message } from '../../../Domain/Chat/ChatAggregate'; -import { logAuth, logError } from '../../Services/Logger'; -import { v4 as uuidv4 } from 'uuid'; - -export class SendMessageCommandHandler { - constructor(private chatRepository: IChatRepository) {} - - async execute(command: SendMessageCommand): Promise { - try { - // Validate message is non-empty string - if (typeof command.message !== 'string' || !command.message.trim()) { - throw new Error('Message must be a non-empty string'); - } - - const chat = await this.chatRepository.findById(command.chatId); - if (!chat) { - throw new Error('Chat not found'); - } - - // Check if user is member of this chat - if (!chat.users.includes(command.userId)) { - throw new Error('User is not a member of this chat'); - } - - // Create message - const message: Message = { - id: uuidv4(), - date: new Date(), - userid: command.userId, - text: command.message.trim() - }; - - // Manage message history (keep last 10 per user, up to 2 weeks) - let updatedMessages = [...chat.messages, message]; - updatedMessages = this.pruneMessages(updatedMessages); - - // Update chat - await this.chatRepository.update(command.chatId, { - messages: updatedMessages, - lastActivity: new Date() - }); - - logAuth('Message sent successfully', command.userId, { - chatId: command.chatId, - messageLength: command.message.length, - totalMessages: updatedMessages.length - }); - - return message; - - } catch (error) { - logError('SendMessageCommandHandler error', error as Error); - return null; - } - } - - private pruneMessages(messages: Message[]): Message[] { - const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000); - - // Remove messages older than 2 weeks - let prunedMessages = messages.filter(msg => new Date(msg.date) > twoWeeksAgo); - - // Group by user and keep last 10 messages per user - const messagesByUser = new Map(); - prunedMessages.forEach(msg => { - if (!messagesByUser.has(msg.userid)) { - messagesByUser.set(msg.userid, []); - } - messagesByUser.get(msg.userid)!.push(msg); - }); - - // Keep only last 10 messages per user - const finalMessages: Message[] = []; - messagesByUser.forEach((userMessages, userId) => { - const last10 = userMessages.slice(-10); - finalMessages.push(...last10); - }); - - // Sort by date - return finalMessages.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/commands/SoftDeleteCommandHandlers.ts b/SerpentRace_Backend/src/Application/Chat/commands/SoftDeleteCommandHandlers.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/SerpentRace_Backend/src/Application/Chat/queries/ChatHistoryQueryHandlers.ts b/SerpentRace_Backend/src/Application/Chat/queries/ChatHistoryQueryHandlers.ts deleted file mode 100644 index 5f215037..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/ChatHistoryQueryHandlers.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { GetChatHistoryQuery, GetArchivedChatsQuery } from './ChatQueries'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { IChatArchiveRepository } from '../../../Domain/IRepository/IChatArchiveRepository'; -import { Message } from '../../../Domain/Chat/ChatAggregate'; -import { logAuth, logError, logWarning } from '../../Services/Logger'; - -interface ChatHistoryResult { - chatId: string; - messages: Message[]; - isArchived: boolean; - chatInfo: { - type: string; - name: string | null; - gameId: string | null; - users: string[]; - }; -} - -export class GetChatHistoryQueryHandler { - constructor( - private chatRepository: IChatRepository, - private chatArchiveRepository: IChatArchiveRepository - ) {} - - async execute(query: GetChatHistoryQuery): Promise { - try { - // First try to find active chat - const chat = await this.chatRepository.findById(query.chatId); - - if (chat) { - // Check authorization - if (!chat.users.includes(query.userId)) { - logWarning('Unauthorized chat history access attempt', { - chatId: query.chatId, - userId: query.userId - }); - return null; - } - - logAuth('Chat history retrieved', query.userId, { - chatId: query.chatId, - messageCount: chat.messages.length, - isArchived: false - }); - - return { - chatId: query.chatId, - messages: chat.messages, - isArchived: false, - chatInfo: { - type: chat.type, - name: chat.name, - gameId: chat.gameId, - users: chat.users - } - }; - } - - // Try to find in archives - const archives = await this.chatArchiveRepository.findByChatId(query.chatId); - const userArchive = archives.find(archive => - archive.participants.includes(query.userId) - ); - - if (userArchive) { - logAuth('Archived chat history retrieved', query.userId, { - chatId: query.chatId, - messageCount: userArchive.archivedMessages.length, - isArchived: true - }); - - return { - chatId: query.chatId, - messages: userArchive.archivedMessages, - isArchived: true, - chatInfo: { - type: userArchive.chatType, - name: userArchive.chatName, - gameId: userArchive.gameId, - users: userArchive.participants - } - }; - } - - logWarning('Chat history not found', { - chatId: query.chatId, - userId: query.userId - }); - - return null; - - } catch (error) { - logError('GetChatHistoryQueryHandler error', error as Error); - return null; - } - } -} - -export class GetArchivedChatsQueryHandler { - constructor(private chatArchiveRepository: IChatArchiveRepository) {} - - async execute(query: GetArchivedChatsQuery): Promise { - try { - let archives: any[] = []; - - if (query.gameId) { - // Get archived game chats - archives = await this.chatArchiveRepository.findByGameId(query.gameId); - } else { - // Get all archived chats for user (would need different query) - // For now, return empty - this would need a new repository method - archives = []; - } - - const result = archives - .filter(archive => archive.participants.includes(query.userId)) - .map(archive => ({ - chatId: archive.chatId, - messages: archive.archivedMessages, - isArchived: true, - chatInfo: { - type: archive.chatType, - name: archive.chatName, - gameId: archive.gameId, - users: archive.participants - } - })); - - logAuth('Archived chats retrieved', query.userId, { - count: result.length, - gameId: query.gameId - }); - - return result; - - } catch (error) { - logError('GetArchivedChatsQueryHandler error', error as Error); - return []; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/queries/ChatQueries.ts b/SerpentRace_Backend/src/Application/Chat/queries/ChatQueries.ts deleted file mode 100644 index 69e0b36c..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/ChatQueries.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface GetUserChatsQuery { - userId: string; - includeArchived?: boolean; -} - -export interface GetChatHistoryQuery { - chatId: string; - userId: string; // For authorization -} - -export interface GetArchivedChatsQuery { - userId: string; - gameId?: string; -} diff --git a/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQuery.ts b/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQuery.ts deleted file mode 100644 index 18fc595d..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQuery.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GetChatsByPageQuery { - from: number; - to: number; - includeDeleted?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQueryHandler.ts deleted file mode 100644 index fbafa33c..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/GetChatsByPageQueryHandler.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { GetChatsByPageQuery } from './GetChatsByPageQuery'; -import { ShortChatDto } from '../../DTOs/ChatDto'; -import { ChatMapper } from '../../DTOs/Mappers/ChatMapper'; -import { logRequest, logError } from '../../Services/Logger'; - -export class GetChatsByPageQueryHandler { - constructor(private readonly chatRepo: IChatRepository) {} - - async execute(query: GetChatsByPageQuery): Promise<{ chats: ShortChatDto[], totalCount: number }> { - try { - // Validate pagination parameters - if (query.from < 0 || query.to < query.from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = query.to - query.from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - logRequest('Get chats by page query started', undefined, undefined, { - from: query.from, - to: query.to, - includeDeleted: query.includeDeleted || false - }); - - const result = query.includeDeleted - ? await this.chatRepo.findByPageIncludingDeleted(query.from, query.to) - : await this.chatRepo.findByPage(query.from, query.to); - - logRequest('Get chats by page query completed', undefined, undefined, { - from: query.from, - to: query.to, - returned: result.chats.length, - totalCount: result.totalCount, - includeDeleted: query.includeDeleted || false - }); - - return { - chats: ChatMapper.toShortDtoList(result.chats), - totalCount: result.totalCount - }; - } catch (error) { - logError('GetChatsByPageQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Re-throw validation errors as-is - if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) { - throw error; - } - - throw new Error('Failed to retrieve chats page'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Chat/queries/GetUserChatsQueryHandler.ts b/SerpentRace_Backend/src/Application/Chat/queries/GetUserChatsQueryHandler.ts deleted file mode 100644 index abb6fad0..00000000 --- a/SerpentRace_Backend/src/Application/Chat/queries/GetUserChatsQueryHandler.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { GetUserChatsQuery } from './ChatQueries'; -import { IChatRepository } from '../../../Domain/IRepository/IChatRepository'; -import { IChatArchiveRepository } from '../../../Domain/IRepository/IChatArchiveRepository'; -import { ChatAggregate } from '../../../Domain/Chat/ChatAggregate'; -import { ChatArchiveAggregate } from '../../../Domain/Chat/ChatArchiveAggregate'; -import { logAuth, logError } from '../../Services/Logger'; - -interface ChatWithMetadata { - id: string; - type: string; - name: string | null; - gameId: string | null; - users: string[]; - lastActivity: Date | null; - isArchived: boolean; - messageCount: number; - unreadCount?: number; -} - -export class GetUserChatsQueryHandler { - constructor( - private chatRepository: IChatRepository, - private chatArchiveRepository: IChatArchiveRepository - ) {} - - async execute(query: GetUserChatsQuery): Promise { - try { - const result: ChatWithMetadata[] = []; - - // Get active chats - const activeChats = await this.chatRepository.findActiveChatsForUser(query.userId); - result.push(...activeChats.map(chat => ({ - id: chat.id, - type: chat.type, - name: chat.name, - gameId: chat.gameId, - users: chat.users, - lastActivity: chat.lastActivity, - isArchived: false, - messageCount: chat.messages.length, - unreadCount: this.calculateUnreadMessages(chat, query.userId) - }))); - - // Get archived chats if requested - if (query.includeArchived) { - const userActiveChats = await this.chatRepository.findByUserId(query.userId); - const archivedChatIds = userActiveChats - .filter(chat => chat.archiveDate !== null) - .map(chat => chat.id); - - const archives = await Promise.all( - archivedChatIds.map(id => this.chatArchiveRepository.findByChatId(id)) - ); - - archives.forEach(archiveArray => { - archiveArray.forEach(archive => { - if (archive.participants.includes(query.userId)) { - result.push({ - id: archive.chatId, - type: archive.chatType, - name: archive.chatName, - gameId: archive.gameId, - users: archive.participants, - lastActivity: archive.archivedAt, - isArchived: true, - messageCount: archive.archivedMessages.length, - unreadCount: 0 // Archived chats have no unread messages - }); - } - }); - }); - } - - logAuth('User chats retrieved', query.userId, { - activeCount: activeChats.length, - totalCount: result.length, - includeArchived: query.includeArchived - }); - - return result.sort((a, b) => { - if (!a.lastActivity) return 1; - if (!b.lastActivity) return -1; - return new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime(); - }); - - } catch (error) { - logError('GetUserChatsQueryHandler error', error as Error); - return []; - } - } - - private calculateUnreadMessages(chat: ChatAggregate, userId: string): number { - // Simple implementation - count messages from other users - // In production, you'd store lastSeen timestamp per user per chat - return chat.messages.filter(msg => msg.userid !== userId).length; - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommand.ts b/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommand.ts deleted file mode 100644 index 54ceeee5..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommand.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ContactType } from '../../../Domain/Contact/ContactAggregate'; - -export interface CreateContactCommand { - name: string; - email: string; - userid?: string; - type: ContactType; - txt: string; -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommandHandler.ts b/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommandHandler.ts deleted file mode 100644 index ddc2a40b..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/CreateContactCommandHandler.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { CreateContactCommand } from './CreateContactCommand'; -import { ShortContactDto } from '../../DTOs/ContactDto'; -import { ContactAggregate, ContactState } from '../../../Domain/Contact/ContactAggregate'; -import { ContactMapper } from '../../DTOs/Mappers/ContactMapper'; - -export class CreateContactCommandHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(cmd: CreateContactCommand): Promise { - try { - const contact = new ContactAggregate(); - contact.name = cmd.name; - contact.email = cmd.email; - contact.userid = cmd.userid || null; - contact.type = cmd.type; - contact.txt = cmd.txt; - contact.state = ContactState.ACTIVE; - - const created = await this.contactRepo.create(contact); - return ContactMapper.toShortDto(created); - } catch (error) { - throw new Error('Failed to create contact'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommand.ts b/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommand.ts deleted file mode 100644 index 99b1e0b7..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface DeleteContactCommand { - id: string; - hard?: boolean; // true for permanent delete, false/undefined for soft delete -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommandHandler.ts b/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommandHandler.ts deleted file mode 100644 index c2a3f9f9..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/DeleteContactCommandHandler.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { DeleteContactCommand } from './DeleteContactCommand'; -import { AdminAuditService } from '../../Services/AdminBypassService'; -import { logRequest } from '../../Services/Logger'; - -export class DeleteContactCommandHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(cmd: DeleteContactCommand): Promise { - try { - const existingContact = await this.contactRepo.findById(cmd.id); - if (!existingContact) { - throw new Error('Contact not found'); - } - - if (cmd.hard) { - // Permanent delete - await this.contactRepo.delete(cmd.id); - logRequest('Contact hard deleted', undefined, undefined, { - contactId: cmd.id, - contactEmail: existingContact.email, - deleteType: 'hard' - }); - } else { - // Soft delete (default) - await this.contactRepo.softDelete(cmd.id); - logRequest('Contact soft deleted', undefined, undefined, { - contactId: cmd.id, - contactEmail: existingContact.email, - deleteType: 'soft' - }); - } - - return true; - } catch (error) { - if (error instanceof Error && error.message === 'Contact not found') { - throw error; - } - throw new Error('Failed to delete contact'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommand.ts b/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommand.ts deleted file mode 100644 index 6d66e809..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommand.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface UpdateContactCommand { - id: string; - adminResponse?: string; - state?: number; - respondedBy?: string; -} diff --git a/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommandHandler.ts b/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommandHandler.ts deleted file mode 100644 index e25ec319..00000000 --- a/SerpentRace_Backend/src/Application/Contact/commands/UpdateContactCommandHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { UpdateContactCommand } from './UpdateContactCommand'; -import { DetailContactDto } from '../../DTOs/ContactDto'; -import { ContactMapper } from '../../DTOs/Mappers/ContactMapper'; -import { ContactState } from '../../../Domain/Contact/ContactAggregate'; - -export class UpdateContactCommandHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(cmd: UpdateContactCommand): Promise { - try { - const existingContact = await this.contactRepo.findById(cmd.id); - if (!existingContact) { - throw new Error('Contact not found'); - } - - const updateData: any = {}; - - if (cmd.adminResponse !== undefined) { - updateData.adminResponse = cmd.adminResponse; - updateData.responseDate = new Date(); - } - - if (cmd.state !== undefined) { - updateData.state = cmd.state; - } - - if (cmd.respondedBy !== undefined) { - updateData.respondedBy = cmd.respondedBy; - } - - const updated = await this.contactRepo.update(cmd.id, updateData); - if (!updated) { - throw new Error('Failed to update contact'); - } - - return ContactMapper.toDetailDto(updated); - } catch (error) { - if (error instanceof Error && error.message === 'Contact not found') { - throw error; - } - throw new Error('Failed to update contact'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQuery.ts b/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQuery.ts deleted file mode 100644 index f8686379..00000000 --- a/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetContactByIdQuery { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQueryHandler.ts b/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQueryHandler.ts deleted file mode 100644 index d20f3c7a..00000000 --- a/SerpentRace_Backend/src/Application/Contact/queries/GetContactByIdQueryHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { GetContactByIdQuery } from './GetContactByIdQuery'; -import { DetailContactDto } from '../../DTOs/ContactDto'; -import { ContactMapper } from '../../DTOs/Mappers/ContactMapper'; - -export class GetContactByIdQueryHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(query: GetContactByIdQuery): Promise { - const contact = await this.contactRepo.findById(query.id); - if (!contact) { - return null; - } - return ContactMapper.toDetailDto(contact); - } -} diff --git a/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQuery.ts b/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQuery.ts deleted file mode 100644 index cc5850ab..00000000 --- a/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQuery.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface GetContactsByPageQuery { - from: number; - to: number; -} diff --git a/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQueryHandler.ts deleted file mode 100644 index e39234df..00000000 --- a/SerpentRace_Backend/src/Application/Contact/queries/GetContactsByPageQueryHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IContactRepository } from '../../../Domain/IRepository/IContactRepository'; -import { GetContactsByPageQuery } from './GetContactsByPageQuery'; -import { ContactPageDto } from '../../DTOs/ContactDto'; -import { ContactMapper } from '../../DTOs/Mappers/ContactMapper'; - -export class GetContactsByPageQueryHandler { - constructor(private readonly contactRepo: IContactRepository) {} - - async execute(query: GetContactsByPageQuery): Promise { - const result = await this.contactRepo.findByPage(query.from, query.to); - return { - contacts: ContactMapper.toShortDtoList(result.contacts), - totalCount: result.totalCount, - from: query.from, - to: query.to, - }; - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/ChatDto.ts b/SerpentRace_Backend/src/Application/DTOs/ChatDto.ts deleted file mode 100644 index f3f59bf3..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/ChatDto.ts +++ /dev/null @@ -1,26 +0,0 @@ -export interface CreateChatDto { - users: string[]; - messages: import('../../Domain/Chat/ChatAggregate').Message[]; - state?: number; -} - -export interface UpdateChatDto { - id: string; - users?: string[]; - messages?: import('../../Domain/Chat/ChatAggregate').Message[]; - state?: number; -} - -export interface ShortChatDto { - id: string; - userCount: number; - state: number; -} - -export interface DetailChatDto { - id: string; - users: string[]; - messages: import('../../Domain/Chat/ChatAggregate').Message[]; - updateDate: Date; - state: number; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/ContactDto.ts b/SerpentRace_Backend/src/Application/DTOs/ContactDto.ts deleted file mode 100644 index 44100357..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/ContactDto.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ContactType } from '../../Domain/Contact/ContactAggregate'; - -export interface CreateContactDto { - name: string; - email: string; - userid?: string; - type: ContactType; - txt: string; -} - -export interface UpdateContactDto { - id: string; - adminResponse?: string; - state?: number; - respondedBy?: string; -} - -export interface ShortContactDto { - id: string; - name: string; - email: string; - type: ContactType; - createDate: Date; - state: number; -} - -export interface DetailContactDto { - id: string; - name: string; - email: string; - userid: string | null; - type: ContactType; - txt: string; - state: number; - createDate: Date; - updateDate: Date; - adminResponse: string | null; - responseDate: Date | null; - respondedBy: string | null; -} - -export interface ContactPageDto { - contacts: ShortContactDto[]; - totalCount: number; - from: number; - to: number; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/DeckDto.ts b/SerpentRace_Backend/src/Application/DTOs/DeckDto.ts deleted file mode 100644 index 68bd74d2..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/DeckDto.ts +++ /dev/null @@ -1,33 +0,0 @@ -export interface CreateDeckDto { - name: string; - description?: string; -} - -export interface UpdateDeckDto { - id: string; - name?: string; - description?: string; -} - -export interface ShortDeckDto { - id: string; - name: string; - type: number; - playedNumber: number; - ctype: number; - cardCount: number; - creator: string; - creationdate: Date; - editable?: boolean; -} - -export interface DetailDeckDto { - id: string; - name: string; - type: number; - userid: string; - creationdate: Date; - cards: any[]; - playedNumber: number; - ctype: number; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/GameDto.ts b/SerpentRace_Backend/src/Application/DTOs/GameDto.ts deleted file mode 100644 index 02bc0f61..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/GameDto.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as DeckAggregate from "../../Domain/Deck/DeckAggregate"; - -export interface GameStartDto { - gameid: string; - maxplayers: number; - logintype: number; - gamecode: string; - deck: gamedeck[]; -} - -enum decktype { - JOCKER = 0, - LUCK = 1, - QUEST = 2 -} - -export interface cards { - cardid: string; - question?: string; - answer?: string; - consequence?: DeckAggregate.Consequence | null; - played?: boolean; - playerid?: string; -} - -export interface gamedeck { - deckid: string; - decktype: decktype; - cards: cards[]; -} - -export interface GameDataDto { - id: string; - gamecode: string; - maxplayers: number; - logintype: number; - gamedecks: gamedeck[]; - players: string[]; - started: boolean; - finished: boolean; - winner?: string; - currentplayer?: string; - createdate: Date; - startdate?: Date; - enddate?: Date; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/BaseMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/BaseMapper.ts deleted file mode 100644 index d11c5ea4..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/BaseMapper.ts +++ /dev/null @@ -1,19 +0,0 @@ -export abstract class BaseMapper { - abstract toShortDto(entity: TEntity): TShortDto; - abstract toDetailDto(entity: TEntity): TDetailDto; - - toShortDtoList(entities: TEntity[]): TShortDto[] { - return entities.map(entity => this.toShortDto(entity)); - } - - toDetailDtoList(entities: TEntity[]): TDetailDto[] { - return entities.map(entity => this.toDetailDto(entity)); - } - - static toShortDtoListStatic( - entities: T[], - mapperFn: (entity: T) => TDto - ): TDto[] { - return entities.map(mapperFn); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/ChatMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/ChatMapper.ts deleted file mode 100644 index 60507b2f..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/ChatMapper.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ChatAggregate } from '../../../Domain/Chat/ChatAggregate'; -import { ShortChatDto, DetailChatDto } from '../ChatDto'; - -export class ChatMapper { - static toShortDto(chat: ChatAggregate): ShortChatDto { - return { - id: chat.id, - userCount: chat.users?.length ?? 0, - state: chat.state, - }; - } - - static toDetailDto(chat: ChatAggregate): DetailChatDto { - return { - id: chat.id, - users: chat.users ?? [], - messages: chat.messages, - updateDate: chat.updateDate, - state: chat.state, - }; - } - - static toShortDtoList(chats: ChatAggregate[]): ShortChatDto[] { - return chats.map(this.toShortDto); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/ContactMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/ContactMapper.ts deleted file mode 100644 index b9a23ed1..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/ContactMapper.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ContactAggregate } from '../../../Domain/Contact/ContactAggregate'; -import { CreateContactDto, UpdateContactDto, ShortContactDto, DetailContactDto } from '../ContactDto'; - -export class ContactMapper { - static toShortDto(contact: ContactAggregate): ShortContactDto { - return { - id: contact.id, - name: contact.name, - email: contact.email, - type: contact.type, - createDate: contact.createDate, - state: contact.state, - }; - } - - static toDetailDto(contact: ContactAggregate): DetailContactDto { - return { - id: contact.id, - name: contact.name, - email: contact.email, - userid: contact.userid, - type: contact.type, - txt: contact.txt, - state: contact.state, - createDate: contact.createDate, - updateDate: contact.updateDate, - adminResponse: contact.adminResponse, - responseDate: contact.responseDate, - respondedBy: contact.respondedBy, - }; - } - - static toShortDtoList(contacts: ContactAggregate[]): ShortContactDto[] { - return contacts.map(this.toShortDto); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/DeckMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/DeckMapper.ts deleted file mode 100644 index 87946870..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/DeckMapper.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; -import { UserAggregate } from '../../../Domain/User/UserAggregate'; -import { CreateDeckDto, UpdateDeckDto, ShortDeckDto, DetailDeckDto } from '../DeckDto'; - -export class DeckMapper { - static toShortDto(deck: DeckAggregate, userId?: string): ShortDeckDto { - return { - id: deck.id, - name: deck.name, - type: deck.type, - playedNumber: deck.playedNumber, - ctype: deck.ctype, - cardCount: deck.cards.length, - creator: deck.user?.username || 'Unknown', - creationdate: deck.creationdate, - editable: deck.isEditable(userId!) ? deck.isEditable(userId!) : undefined - }; - } - - static toDetailDto(deck: DeckAggregate): DetailDeckDto { - return { - id: deck.id, - name: deck.name, - type: deck.type, - userid: deck.userid, - creationdate: deck.creationdate, - cards: deck.cards, - playedNumber: deck.playedNumber, - ctype: deck.ctype, - }; - } - - static toShortDtoList(decks: DeckAggregate[], userId?: string): ShortDeckDto[] { - return decks.map(deck => ({ - id: deck.id, - name: deck.name, - type: deck.type, - playedNumber: deck.playedNumber, - ctype: deck.ctype, - cardCount: deck.cards.length, - creator: deck.user?.username || 'Unknown', - creationdate: deck.creationdate, - editable: deck.isEditable(userId!) ? deck.isEditable(userId!) : undefined - })); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/OrganizationMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/OrganizationMapper.ts deleted file mode 100644 index 70ff5352..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/OrganizationMapper.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { OrganizationAggregate } from '../../../Domain/Organization/OrganizationAggregate'; -import { CreateOrganizationDto, UpdateOrganizationDto, ShortOrganizationDto, DetailOrganizationDto } from '../OrganizationDto'; - -export class OrganizationMapper { - static toShortDto(org: OrganizationAggregate): ShortOrganizationDto { - return { - id: org.id, - name: org.name, - state: org.state, - userinorg: org.userinorg, - maxOrganizationalDecks: org.maxOrganizationalDecks, - }; - } - - static toDetailDto(org: OrganizationAggregate): DetailOrganizationDto { - return { - id: org.id, - name: org.name, - contactfname: org.contactfname, - contactlname: org.contactlname, - contactphone: org.contactphone, - contactemail: org.contactemail, - state: org.state, - regdate: org.regdate, - updateDate: org.updateDate, - url: org.url, - userinorg: org.userinorg, - maxOrganizationalDecks: org.maxOrganizationalDecks, - users: org.users?.map(u => u.id) ?? [], - }; - } - - static toShortDtoList(orgs: OrganizationAggregate[]): ShortOrganizationDto[] { - return orgs.map(this.toShortDto); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/Mappers/UserMapper.ts b/SerpentRace_Backend/src/Application/DTOs/Mappers/UserMapper.ts deleted file mode 100644 index 877ccc0f..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/Mappers/UserMapper.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { UserAggregate, UserState } from '../../../Domain/User/UserAggregate'; -import { CreateUserDto, UpdateUserDto, ShortUserDto, DetailUserDto } from '../UserDto'; -import { BaseMapper } from './BaseMapper'; - -export class UserMapper { - static toShortDto(user: UserAggregate): ShortUserDto { - return { - username: user.username, - authLevel: (user.state === UserState.ADMIN ? 1 : 0) as 0 | 1, - }; - } - - static toDetailDto(user: UserAggregate): DetailUserDto { - return { - id: user.id, - orgid: user.orgid, - username: user.username, - email: user.email, - fname: user.fname, - lname: user.lname, - code: user.token, - phone: user.phone, - state: user.state, - }; - } - - static toShortDtoList(users: UserAggregate[]): ShortUserDto[] { - return BaseMapper.toShortDtoListStatic(users, UserMapper.toShortDto); - } -} diff --git a/SerpentRace_Backend/src/Application/DTOs/OrganizationDto.ts b/SerpentRace_Backend/src/Application/DTOs/OrganizationDto.ts deleted file mode 100644 index c1eb4aaf..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/OrganizationDto.ts +++ /dev/null @@ -1,48 +0,0 @@ -export interface CreateOrganizationDto { - name: string; - description?: string; - maxOrganizationalDecks?: number | null; -} - -export interface UpdateOrganizationDto { - id: string; - name?: string; - description?: string; -} - -export interface ShortOrganizationDto { - id: string; - name: string; - state: number; - userinorg: number; - maxOrganizationalDecks?: number | null; -} - -export interface DetailOrganizationDto { - id: string; - name: string; - contactfname: string; - contactlname: string; - contactphone: string; - contactemail: string; - state: number; - regdate: Date; - updateDate: Date; - url: string | null; - userinorg: number; - maxOrganizationalDecks: number | null; - users: string[]; -} - -export interface OrganizationLoginUrlDto { - organizationId: string; - organizationName: string; - loginUrl: string; -} - -export interface OrganizationAuthCallbackDto { - organizationId: string; - userId: string; - status: 'ok' | 'not_ok'; - authToken?: string; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/SearchDto.ts b/SerpentRace_Backend/src/Application/DTOs/SearchDto.ts deleted file mode 100644 index acb616d8..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/SearchDto.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface SearchQuery { - query: string; - limit?: number; - offset?: number; -} - -export interface SearchResult { - results: T[]; - totalCount: number; - hasMore: boolean; - searchQuery: string; - searchType: 'users' | 'organizations' | 'decks'; -} diff --git a/SerpentRace_Backend/src/Application/DTOs/UserDto.ts b/SerpentRace_Backend/src/Application/DTOs/UserDto.ts deleted file mode 100644 index d3a68ab7..00000000 --- a/SerpentRace_Backend/src/Application/DTOs/UserDto.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface CreateUserDto { - username: string; - email: string; -} - -export interface UpdateUserDto { - id: string; - username?: string; - email?: string; -} - -export interface ShortUserDto { - username: string; - authLevel: 0 | 1; -} - -export interface DetailUserDto { - id: string; - orgid: string | null; - username: string; - email: string; - fname: string; - lname: string; - code: string | null; - phone: string | null; - state: number; -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommand.ts b/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommand.ts deleted file mode 100644 index 9e4ec5ad..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommand.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface CreateDeckCommand { - name: string; - type: number; - userid: string; - cards: any[]; - ctype?: number; -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommandHandler.ts b/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommandHandler.ts deleted file mode 100644 index c6c75d2f..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/CreateDeckCommandHandler.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { CreateDeckCommand } from './CreateDeckCommand'; -import { ShortDeckDto } from '../../DTOs/DeckDto'; -import { DeckAggregate, State, CType } from '../../../Domain/Deck/DeckAggregate'; -import { UserState } from '../../../Domain/User/UserAggregate'; -import { DeckMapper } from '../../DTOs/Mappers/DeckMapper'; -import { AdminBypassService } from '../../Services/AdminBypassService'; -import { logRequest } from '../../Services/Logger'; - -export class CreateDeckCommandHandler { - constructor( - private readonly deckRepo: IDeckRepository, - private readonly userRepo: IUserRepository, - private readonly orgRepo: IOrganizationRepository - ) {} - - async execute(cmd: CreateDeckCommand): Promise { - try { - // 1. Get user details - const user = await this.userRepo.findById(cmd.userid); - if (!user) { - throw new Error('User not found'); - } - - // 2. ADMIN BYPASS - Skip all restrictions - if (AdminBypassService.shouldBypassRestrictions(user.state)) { - AdminBypassService.logAdminBypass( - 'CREATE_DECK_BYPASS', - user.id, - 'new-deck', - { - deckName: cmd.name, - deckType: cmd.type, - cardCount: cmd.cards.length, - ctype: cmd.ctype - } - ); - return this.createDeck(cmd); - } - - // 3. Check deck count limits for regular users - const userDeckCount = await this.deckRepo.countActiveByUserId(cmd.userid); - const maxDecks = user.state === UserState.VERIFIED_PREMIUM ? 12 : 8; - - if (userDeckCount >= maxDecks) { - throw new Error(`Deck limit exceeded. Maximum ${maxDecks} decks allowed for your account type.`); - } - - // 4. Organizational deck restrictions - if (cmd.ctype === CType.ORGANIZATION) { - // Only premium users can create organizational decks - if (user.state !== UserState.VERIFIED_PREMIUM) { - throw new Error('Only premium users can create organizational decks.'); - } - - // User must belong to an organization - if (!user.orgid) { - throw new Error('You must be a member of an organization to create organizational decks.'); - } - - // Check organization limits - const org = await this.orgRepo.findById(user.orgid); - if (!org) { - throw new Error('Organization not found.'); - } - - if (org.maxOrganizationalDecks === null) { - throw new Error('Organization deck limit not configured. Contact administrator.'); - } - - const userOrgDeckCount = await this.deckRepo.countOrganizationalByUserId(cmd.userid); - if (userOrgDeckCount >= org.maxOrganizationalDecks) { - throw new Error(`Organization deck limit exceeded. Maximum ${org.maxOrganizationalDecks} organizational decks allowed.`); - } - } - - // 5. Create deck with restrictions passed - return this.createDeck(cmd); - } catch (error) { - if (error instanceof Error) { - throw error; // Re-throw known errors with original message - } - throw new Error('Failed to create deck'); - } - } - - /** - * Private method to create deck after all validations - */ - private async createDeck(cmd: CreateDeckCommand): Promise { - const deck = new DeckAggregate(); - deck.name = cmd.name; - deck.type = cmd.type; - deck.userid = cmd.userid; - deck.cards = cmd.cards; - deck.ctype = cmd.ctype ?? CType.PUBLIC; - deck.state = State.ACTIVE; - - // Set organization reference for organizational decks - if (cmd.ctype === CType.ORGANIZATION) { - const user = await this.userRepo.findById(cmd.userid); - if (user?.orgid) { - const org = await this.orgRepo.findById(user.orgid); - if (org) { - deck.organization = org; - } - } - } - - const created = await this.deckRepo.create(deck); - - logRequest('Deck created successfully', undefined, undefined, { - deckId: created.id, - userId: cmd.userid, - deckName: cmd.name, - deckType: cmd.type, - ctype: cmd.ctype, - cardCount: cmd.cards.length - }); - - return DeckMapper.toShortDto(created); - } -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommand.ts b/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommand.ts deleted file mode 100644 index d41de235..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommand.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface DeleteDeckCommand { - userid: string; - authLevel: number; - id: string; - soft?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommandHandler.ts b/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommandHandler.ts deleted file mode 100644 index 07309b19..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/DeleteDeckCommandHandler.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { logAuth, logError } from '../../Services/Logger'; -import { DeleteDeckCommand } from './DeleteDeckCommand'; - -export class DeleteDeckCommandHandler { - constructor(private readonly deckRepo: IDeckRepository) {} - - async execute(cmd: DeleteDeckCommand): Promise { - - //get decks userid - const deck = await this.deckRepo.findById(cmd.id); - if (!deck) { - logError(`Deck not found with ID: ${cmd.id}`); - throw new Error('Deck not found'); - } - - if(cmd.authLevel !==1 && deck.userid !== cmd.userid) { - logAuth(`Unauthorized access attempt to deck with ID: ${cmd.id}, UserID: ${cmd.userid}`); - throw new Error('Unauthorized'); - } - - if (cmd.soft) { - await this.deckRepo.softDelete(cmd.id); - } else { - await this.deckRepo.delete(cmd.id); - } - return true; - } -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommand.ts b/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommand.ts deleted file mode 100644 index 9fbbb3a9..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommand.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface UpdateDeckCommand { - userid: string; - authLevel: number; - id: string; - userstate?: number; - name?: string; - type?: number; - cards?: any[]; - ctype?: number; - state?: number; -} diff --git a/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommandHandler.ts b/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommandHandler.ts deleted file mode 100644 index 85cc4202..00000000 --- a/SerpentRace_Backend/src/Application/Deck/commands/UpdateDeckCommandHandler.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { UpdateDeckCommand } from './UpdateDeckCommand'; -import { ShortDeckDto } from '../../DTOs/DeckDto'; -import { DeckMapper } from '../../DTOs/Mappers/DeckMapper'; -import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; -import { logAuth, logError } from '../../Services/Logger'; - -export class UpdateDeckCommandHandler { - constructor(private readonly deckRepo: IDeckRepository) {} - - async execute(cmd: UpdateDeckCommand): Promise { - if(cmd.state !== undefined && cmd.authLevel !== 1) { - throw new Error('Only admin users can change deck state'); - } - try { - let existingDeck: DeckAggregate | null = null; - if (cmd.authLevel === 1) { - existingDeck = await this.deckRepo.findByIdIncludingDeleted(cmd.id); - } else { - existingDeck = await this.deckRepo.findById(cmd.id); - } - if (!existingDeck) { - logError(`Deck not found with ID: ${cmd.id}`); - throw new Error('Deck not found'); - } - - if(cmd.authLevel !== 1 && existingDeck.userid !== cmd.userid) { - logAuth(`Unauthorized access attempt to deck with ID: ${cmd.id}, UserID: ${cmd.userid}`); - throw new Error('Unauthorized'); - } - - const for_update: Partial = {}; - if(cmd.name !== undefined) for_update.name = cmd.name; - if(cmd.type !== undefined) for_update.type = cmd.type; - if(cmd.cards !== undefined) for_update.cards = cmd.cards; - if(cmd.ctype !== undefined) for_update.ctype = cmd.ctype; - if(cmd.state !== undefined) for_update.state = cmd.state; - - // Ensure we have something to update - if (Object.keys(for_update).length === 0) { - throw new Error('No fields provided for update'); - } - - const deck = await this.deckRepo.update(cmd.id, { ...for_update }); - if(!deck) { - logError(`Deck update failed for ID: ${cmd.id}. Update returned null.`); - throw new Error('Failed to update deck'); - } - return DeckMapper.toShortDto(deck); - } catch (error: any) { - logError(`Error updating deck: ${cmd.id}`, error); - throw error; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQuery.ts b/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQuery.ts deleted file mode 100644 index 49c192e0..00000000 --- a/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetDeckByIdQuery { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQueryHandler.ts b/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQueryHandler.ts deleted file mode 100644 index 9ea429b4..00000000 --- a/SerpentRace_Backend/src/Application/Deck/queries/GetDeckByIdQueryHandler.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { GetDeckByIdQuery } from './GetDeckByIdQuery'; -import { DetailDeckDto } from '../../DTOs/DeckDto'; -import { DeckMapper } from '../../DTOs/Mappers/DeckMapper'; - -export class GetDeckByIdQueryHandler { - constructor(private readonly deckRepo: IDeckRepository) {} - - async execute(query: GetDeckByIdQuery): Promise { - const deck = await this.deckRepo.findById(query.id); - if (!deck) return null; - return DeckMapper.toDetailDto(deck); - } -} diff --git a/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQuery.ts b/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQuery.ts deleted file mode 100644 index 370fe350..00000000 --- a/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQuery.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface GetDecksByPageQuery { - from: number; - to: number; - userId: string; - userOrgId?: string; - isAdmin: boolean; - includeDeleted?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQueryHandler.ts deleted file mode 100644 index a4e6086a..00000000 --- a/SerpentRace_Backend/src/Application/Deck/queries/GetDecksByPageQueryHandler.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { GetDecksByPageQuery } from './GetDecksByPageQuery'; -import { ShortDeckDto } from '../../DTOs/DeckDto'; -import { DeckMapper } from '../../DTOs/Mappers/DeckMapper'; -import { AdminBypassService } from '../../Services/AdminBypassService'; -import { logRequest, logError } from '../../Services/Logger'; - -export class GetDecksByPageQueryHandler { - constructor(private readonly deckRepo: IDeckRepository) {} - - async execute(query: GetDecksByPageQuery): Promise<{ decks: ShortDeckDto[], totalCount: number }> { - try { - // Validate pagination parameters - if (query.from < 0 || query.to < query.from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = query.to - query.from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - // Log admin bypass if applicable - if (query.isAdmin) { - AdminBypassService.logAdminBypass( - 'GET_DECKS_PAGE_BYPASS', - query.userId, - 'paginated-decks', - { - from: query.from, - to: query.to, - includesDeleted: query.includeDeleted || false, - operation: 'read' - } - ); - } - - logRequest('Get decks by page query started', undefined, undefined, { - userId: query.userId, - userOrgId: query.userOrgId, - isAdmin: query.isAdmin, - from: query.from, - to: query.to, - includeDeleted: query.includeDeleted || false - }); - - // Use paginated filtered deck finding method - const result = await this.deckRepo.findFilteredDecks( - query.userId, - query.userOrgId, - query.isAdmin, - query.from, - query.to - ); - - logRequest('Get decks by page query completed', undefined, undefined, { - userId: query.userId, - userOrgId: query.userOrgId, - isAdmin: query.isAdmin, - from: query.from, - to: query.to, - returned: result.decks.length, - totalCount: result.totalCount, - includeDeleted: query.includeDeleted || false - }); - - return { - decks: DeckMapper.toShortDtoList(result.decks, query.userId), - totalCount: result.totalCount - }; - } catch (error) { - logError('GetDecksByPageQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Re-throw validation errors as-is - if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) { - throw error; - } - - throw new Error('Failed to retrieve decks page'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Game/BoardGenerationService.ts b/SerpentRace_Backend/src/Application/Game/BoardGenerationService.ts deleted file mode 100644 index e952bfe7..00000000 --- a/SerpentRace_Backend/src/Application/Game/BoardGenerationService.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { GameField, BoardData } from '../../Domain/Game/GameAggregate'; -import { logOther, logError } from '../Services/Logger'; - -interface SpecialFieldInfo { - position: number; - type: 'positive' | 'negative' | 'luck'; -} - -export class BoardGenerationService { - async generateBoard( - positiveFieldCount: number, - negativeFieldCount: number, - luckFieldCount: number - ): Promise { - // Pattern-based approach has 100% success rate, no retry needed - const result = this.generateSingleAttempt(positiveFieldCount, negativeFieldCount, luckFieldCount); - - logOther('Pattern-based board generation completed', { - totalFields: result.fields.length, - specialFields: result.fields.filter((f: GameField) => f.type !== 'regular').length, - positiveFields: result.fields.filter((f: GameField) => f.type === 'positive').length, - negativeFields: result.fields.filter((f: GameField) => f.type === 'negative').length, - luckFields: result.fields.filter((f: GameField) => f.type === 'luck').length - }); - - return result; - } - - private generateSingleAttempt( - positiveFieldCount: number, - negativeFieldCount: number, - luckFieldCount: number - ): BoardData { - // Step 1: Choose special field positions - const specialFieldPositions = this.chooseSpecialFieldPositions( - positiveFieldCount, - negativeFieldCount, - luckFieldCount - ); - - // Step 2: Calculate step values using pattern-based approach - const fields = this.calculatePatternBasedStepValues(specialFieldPositions); - - return { - fields - }; - } - - private chooseSpecialFieldPositions( - positiveFieldCount: number, - negativeFieldCount: number, - luckFieldCount: number - ): SpecialFieldInfo[] { - const totalSpecial = positiveFieldCount + negativeFieldCount + luckFieldCount; - const specialFields: SpecialFieldInfo[] = []; - - // Generate unique random positions - const positions = new Set(); - while (positions.size < totalSpecial) { - const position = Math.floor(Math.random() * 100) + 1; // 1-100 - positions.add(position); - } - - // Convert to sorted array - const sortedPositions = Array.from(positions).sort((a, b) => a - b); - - // Distribute types randomly - const types: ('positive' | 'negative' | 'luck')[] = [ - ...Array(positiveFieldCount).fill('positive'), - ...Array(negativeFieldCount).fill('negative'), - ...Array(luckFieldCount).fill('luck') - ]; - - // Shuffle types - for (let i = types.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [types[i], types[j]] = [types[j], types[i]]; - } - - sortedPositions.forEach((position, index) => { - specialFields.push({ - position, - type: types[index] || 'positive' - }); - }); - - return specialFields; - } - - private calculatePatternBasedStepValues(specialFields: SpecialFieldInfo[]): GameField[] { - // Initialize all fields as regular - const fields: GameField[] = Array.from({ length: 100 }, (_, i) => ({ - position: i + 1, - type: 'regular' as const - })); - - // Update special fields with pattern-based step values - specialFields.forEach(specialField => { - const fieldIndex = specialField.position - 1; // Convert to 0-based index - fields[fieldIndex].type = specialField.type; - - if (specialField.type === 'luck') { - // Luck fields don't need step values - return; - } - - // Calculate step values based on position rules - let maxStepValue: number; - let minStepValue: number; - - if (specialField.position <= 80) { - // Positions 1-80: step values can be ±20 - maxStepValue = 20; - minStepValue = -20; - } else { - // Positions 81-100: step values can be -30 to +10 - maxStepValue = 10; - minStepValue = -30; - } - - // Generate appropriate step value for field type - if (specialField.type === 'positive') { - // Positive fields: use positive step values (1-3 range for balanced gameplay) - // Max movement: 3 × 6 (dice) = 18 steps - const stepValue = Math.floor(Math.random() * 3) + 1; // 1-3 - fields[fieldIndex].stepValue = Math.min(stepValue, maxStepValue); - } else { - // Negative fields: use negative step values (-1 to -3 range) - // Max backward: -3 × 6 (dice) = -18 steps - const stepValue = -(Math.floor(Math.random() * 3) + 1); // -1 to -3 - fields[fieldIndex].stepValue = Math.max(stepValue, minStepValue); - } - }); - - return fields; - } - - // This method can be used by FieldEffectService for movement calculations - public calculatePatternBasedMovement( - currentPosition: number, - stepValue: number, - diceValue: number - ): number { - // Calculate pattern modifier based on current position - const patternModifier = this.getPatternModifier(currentPosition, stepValue > 0); - - // Calculate final position: currentPosition + (stepValue × dice) + patternModifier - const movement = stepValue * diceValue; - let finalPosition = currentPosition + movement + patternModifier; - - // Ensure position stays within board bounds (1-100) - if (finalPosition < 1) { - finalPosition = 1; - } else if (finalPosition > 100) { - finalPosition = 100; - } - - return finalPosition; - } - - public getPatternModifier(position: number, positiveField: boolean): number { - // Pattern modifiers STACK for strategic complexity: - // - Positions ending in 0 (10, 20, 30...): No modifier - // - Positions ending in 5 (15, 25, 35...): ±3 modifier - // - Positions divisible by 3 (9, 12, 21...): ±2 modifier - // - Odd positions (1, 7, 11...): ±1 modifier - // Multiple conditions can apply and stack - - if (position % 10 === 0) { - return 0; // Positions ending in 0 - no modifier - } - - let modifier = 0; - const direction = positiveField ? 1 : -1; - - // Check each condition and stack modifiers - if (position % 10 === 5) { - modifier += 3 * direction; // Positions ending in 5 - } - if (position % 3 === 0) { - modifier += 2 * direction; // Divisible by 3 - } - if (position % 2 === 1) { - modifier += 1 * direction; // Odd positions - } - - return modifier; - } - - private validate20_30Rule(currentPosition: number, targetPosition: number, distance: number): boolean { - // Fields 1-85: max 20 fields in any direction - if (currentPosition <= 85) { - return distance <= 20; - } - - // Fields 86-100: max 30 fields backward, max 20 fields forward - if (currentPosition > 85) { - if (targetPosition > currentPosition) { - // Moving forward: max 20 fields - return distance <= 20; - } else { - // Moving backward: max 30 fields - return distance <= 30; - } - } - - return false; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/GameService.ts b/SerpentRace_Backend/src/Application/Game/GameService.ts deleted file mode 100644 index dd941e6a..00000000 --- a/SerpentRace_Backend/src/Application/Game/GameService.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { StartGameCommand } from './commands/StartGameCommand'; -import { StartGameCommandHandler } from './commands/StartGameCommandHandler'; -import { JoinGameCommand } from './commands/JoinGameCommand'; -import { JoinGameCommandHandler } from './commands/JoinGameCommandHandler'; -import { StartGamePlayCommand } from './commands/StartGamePlayCommand'; -import { StartGamePlayCommandHandler, GameStartResult } from './commands/StartGamePlayCommandHandler'; -import { GameAggregate, LoginType } from '../../Domain/Game/GameAggregate'; -import { logOther, logError } from '../Services/Logger'; - -export class GameService { - private startGameHandler: StartGameCommandHandler; - private joinGameHandler: JoinGameCommandHandler; - private startGamePlayHandler: StartGamePlayCommandHandler; - - constructor() { - this.startGameHandler = new StartGameCommandHandler(); - this.joinGameHandler = new JoinGameCommandHandler(); - this.startGamePlayHandler = new StartGamePlayCommandHandler(); - } - - /** - * Starts a new game with the provided deck IDs - * @param deckids Array of deck IDs (should contain 3 types: LUCK, JOKER, QUESTION) - * @param maxplayers Maximum number of players allowed in the game - * @param logintype How players can join the game (PUBLIC, PRIVATE, ORGANIZATION) - * @param userid Optional ID of the user creating the game - * @returns Promise The created game - */ - async startGame( - deckids: string[], - maxplayers: number, - logintype: LoginType, - userid?: string, - orgid?: string | null - ): Promise { - const startTime = performance.now(); - - try { - logOther('GameService.startGame called', { - deckCount: deckids.length, - maxplayers, - logintype, - userid, - orgid - }); - - // Validate input parameters - this.validateStartGameInput(deckids, maxplayers, logintype); - - // Create and execute the command - const command: StartGameCommand = { - deckids, - maxplayers, - logintype, - userid, - orgid - }; - - const game = await this.startGameHandler.handle(command); - - const endTime = performance.now(); - logOther('Game started successfully', { - gameId: game.id, - gameCode: game.gamecode, - deckCount: game.gamedecks.length, - totalCards: game.gamedecks.reduce((sum, deck) => sum + deck.cards.length, 0), - executionTime: Math.round(endTime - startTime) - }); - - return game; - - } catch (error) { - const endTime = performance.now(); - logError('GameService.startGame failed', error instanceof Error ? error : new Error(String(error))); - logOther('Game start failed', { - executionTime: Math.round(endTime - startTime), - error: error instanceof Error ? error.message : String(error) - }); - throw error; - } - } - - /** - * Join an existing game using game code - * @param gameCode 6-character game code - * @param playerId ID of the player joining (optional for public games) - * @param playerName Display name for the player - * @param orgId Organization ID (for organization games) - * @param loginType Type of join being attempted - * @returns Promise The updated game with new player - */ - async joinGame( - gameCode: string, - playerId?: string, - playerName?: string, - orgId?: string | null, - loginType?: LoginType - ): Promise { - const startTime = performance.now(); - - try { - logOther('GameService.joinGame called', { - gameCode, - playerId: playerId || 'anonymous', - playerName, - orgId, - loginType - }); - - // Validate input parameters - this.validateJoinGameInput(gameCode, playerId, loginType); - - // Create and execute the command - const command: JoinGameCommand = { - gameCode, - playerId, - playerName, - orgId, - loginType: loginType || LoginType.PUBLIC - }; - - const game = await this.joinGameHandler.handle(command); - - const endTime = performance.now(); - logOther('Player joined game successfully', { - gameId: game.id, - gameCode: game.gamecode, - playerId, - playerCount: game.players.length, - maxPlayers: game.maxplayers, - executionTime: Math.round(endTime - startTime) - }); - - return game; - - } catch (error) { - const endTime = performance.now(); - logError('GameService.joinGame failed', error instanceof Error ? error : new Error(String(error))); - logOther('Game join failed', { - gameCode, - playerId, - executionTime: Math.round(endTime - startTime), - error: error instanceof Error ? error.message : String(error) - }); - throw error; - } - } - - /** - * Start an existing game (move from WAITING to ACTIVE) - * Initializes all player positions to 0 and assigns random turn order - * @param gameId Game ID to start - * @param userId User ID of the game master (optional for public games) - * @returns Promise The updated game - */ - async startGamePlay( - gameId: string, - userId?: string - ): Promise { - const startTime = performance.now(); - - try { - logOther('GameService.startGamePlay called', { - gameId, - userId: userId || 'system' - }); - - // Validate input parameters - this.validateStartGamePlayInput(gameId); - - // Create and execute the command - const command: StartGamePlayCommand = { - gameId, - userId - }; - - const result = await this.startGamePlayHandler.handle(command); - - const endTime = performance.now(); - logOther('Game play started successfully', { - gameId: result.game.id, - gameCode: result.game.gamecode, - playerCount: result.game.players.length, - gameState: result.game.state, - executionTime: Math.round(endTime - startTime) - }); - - return result; - - } catch (error) { - const endTime = performance.now(); - logError('GameService.startGamePlay failed', error instanceof Error ? error : new Error(String(error))); - logOther('Game play start failed', { - gameId, - userId, - executionTime: Math.round(endTime - startTime), - error: error instanceof Error ? error.message : String(error) - }); - throw error; - } - } - - private validateStartGamePlayInput(gameId: string): void { - // Validate game ID - if (!gameId || typeof gameId !== 'string') { - throw new Error('Game ID is required and must be a string'); - } - - logOther('Start game play input validation passed', { - gameId - }); - } - - private validateJoinGameInput(gameCode: string, playerId?: string, loginType?: LoginType): void { - // Validate game code - if (!gameCode || typeof gameCode !== 'string') { - throw new Error('Game code is required and must be a string'); - } - - if (gameCode.length !== 6) { - throw new Error('Game code must be exactly 6 characters long'); - } - - // Validate login type specific requirements - if (loginType === LoginType.PRIVATE || loginType === LoginType.ORGANIZATION) { - if (!playerId || typeof playerId !== 'string') { - throw new Error(`Player ID is required for ${LoginType[loginType]} games`); - } - } - - logOther('Join game input validation passed', { - gameCode, - playerId: playerId || 'anonymous', - loginType - }); - } - - private validateStartGameInput(deckids: string[], maxplayers: number, logintype: LoginType): void { - // Validate deck IDs - if (!deckids || deckids.length === 0) { - throw new Error('At least one deck ID must be provided'); - } - - if (deckids.length < 3) { - throw new Error('At least 3 decks are required to start a game (one for each type: LUCK, JOKER, QUESTION)'); - } - - // Validate max players - if (!maxplayers || maxplayers < 2) { - throw new Error('Maximum players must be at least 2'); - } - - if (maxplayers > 8) { - throw new Error('Maximum players cannot exceed 8'); - } - - // Validate login type - if (logintype < 0 || logintype > 2) { - throw new Error('Invalid login type. Must be PUBLIC (0), PRIVATE (1), or ORGANIZATION (2)'); - } - - // Check for duplicate deck IDs - const uniqueIds = new Set(deckids); - if (uniqueIds.size !== deckids.length) { - throw new Error('Duplicate deck IDs are not allowed'); - } - - logOther('Start game input validation passed', { - deckCount: deckids.length, - maxplayers, - logintype - }); - } - - /** - * Game flow explanation (to be implemented later): - * - * 1. START GAME (implemented above): - * - Input: deckids, maxplayers, logintype, gamecode - * - Process: Fetch decks, validate types, shuffle cards, create game - * - Output: Game with shuffled deck objects - * - * 2. JOIN GAME (to be implemented): - * - Input: gamecode, playerid - * - Process: Find game, validate capacity, add player - * - Output: Updated game with new player - * - * 3. GAME ROUNDS (to be implemented): - * - Input: gameid, current player - * - Process: Manage turn order, track game state - * - Output: Current player information - * - * 4. PICK CARD (to be implemented): - * - Input: gameid, playerid, deck type - * - Process: Draw card from specific deck, apply consequence - * - Output: Card details and consequence effects - * - * 5. END GAME (to be implemented): - * - Input: gameid, winner - * - Process: Set game as finished, record winner - * - Output: Final game state - */ -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommand.ts b/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommand.ts deleted file mode 100644 index b3c909b4..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommand.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface GenerateBoardCommand { - gameId: string; - positiveFieldCount: number; - negativeFieldCount: number; - luckFieldCount: number; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommandHandler.ts b/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommandHandler.ts deleted file mode 100644 index f5e454a9..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/GenerateBoardCommandHandler.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { GenerateBoardCommand } from './GenerateBoardCommand'; -import { BoardGenerationService } from '../BoardGenerationService'; -import { RedisService } from '../../Services/RedisService'; -import { logOther, logError } from '../../Services/Logger'; -import { BoardData } from '../../../Domain/Game/GameAggregate'; - -export class GenerateBoardCommandHandler { - constructor( - private readonly boardGenerationService: BoardGenerationService, - private readonly redisService: RedisService - ) {} - - async execute(cmd: GenerateBoardCommand): Promise { - try { - logOther(`Starting board generation for game ${cmd.gameId}`); - const startTime = Date.now(); - - // Generate board with 20-30 rule validation - const boardData = await this.boardGenerationService.generateBoard( - cmd.positiveFieldCount, - cmd.negativeFieldCount, - cmd.luckFieldCount - ); - - // Store in Redis - const boardDataWithMetadata: BoardData = { - ...boardData, - gameId: cmd.gameId, - generatedAt: new Date(), - generationComplete: true - }; - - await this.redisService.setWithExpiry( - `game_board_${cmd.gameId}`, - JSON.stringify(boardDataWithMetadata), - 24 * 60 * 60 // 24 hours - ); - - const executionTime = Date.now() - startTime; - logOther(`Board generation completed for game ${cmd.gameId} in ${executionTime}ms using pattern-based approach`); - - } catch (error) { - logError(`Board generation failed for game ${cmd.gameId}:`, error as Error); - - // Store error state in Redis - const errorData: BoardData = { - gameId: cmd.gameId, - fields: [], - generationComplete: false, - error: error instanceof Error ? error.message : 'Unknown error', - generatedAt: new Date() - }; - - await this.redisService.setWithExpiry( - `game_board_${cmd.gameId}`, - JSON.stringify(errorData), - 24 * 60 * 60 - ); - - throw error; - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommand.ts b/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommand.ts deleted file mode 100644 index b59e633c..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommand.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { LoginType } from '../../../Domain/Game/GameAggregate'; - -export interface JoinGameCommand { - gameCode: string; // 6-character game code - playerId?: string; // User ID of the player joining (optional for public games) - playerName?: string; // Display name for the player (required for public games) - orgId?: string | null; // Organization ID (for organization games) - loginType: LoginType; // Type of join being attempted -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommandHandler.ts b/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommandHandler.ts deleted file mode 100644 index 2fa4dc6a..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/JoinGameCommandHandler.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { JoinGameCommand } from './JoinGameCommand'; -import { GameAggregate, GameState, LoginType } from '../../../Domain/Game/GameAggregate'; -import { IGameRepository } from '../../../Domain/IRepository/IGameRepository'; -import { DIContainer } from '../../Services/DIContainer'; -import { RedisService } from '../../Services/RedisService'; -import { logOther, logError } from '../../Services/Logger'; -import { v4 as uuidv4 } from 'uuid'; - -export interface GamePlayerData { - playerId: string; - playerName?: string; - joinedAt: Date; - isOnline: boolean; - position?: number; // For game board position (to be used later) -} - -export interface ActiveGameData { - gameId: string; - gameCode: string; - hostId?: string; - maxPlayers: number; - currentPlayers: GamePlayerData[]; - state: GameState; - createdAt: Date; - startedAt?: Date; - currentTurn?: string; // Player ID whose turn it is - websocketRoom: string; // WebSocket room name for real-time updates -} - -export class JoinGameCommandHandler { - private gameRepository: IGameRepository; - private redisService: RedisService; - - constructor() { - this.gameRepository = DIContainer.getInstance().gameRepository; - this.redisService = RedisService.getInstance(); - } - - async handle(command: JoinGameCommand): Promise { - const startTime = performance.now(); - - try { - logOther('Joining game', `gameCode: ${command.gameCode}, playerId: ${command.playerId || 'anonymous'}, loginType: ${command.loginType}`); - - // Find the game by game code - const game = await this.gameRepository.findByGameCode(command.gameCode); - if (!game) { - throw new Error(`Game with code ${command.gameCode} not found`); - } - - // Generate player ID for public games or use provided one - // For anonymous players (no playerId), use playerName as the identifier to allow rejoining - const actualPlayerId = command.playerId || `guest_${command.playerName}`; - - // Validate game joinability (authentication/org checks done in router) - this.validateGameJoinability(game, actualPlayerId, command); - - // Add player to database - const updatedGame = await this.gameRepository.addPlayerToGame(game.id, actualPlayerId); - if (!updatedGame) { - throw new Error('Failed to add player to game'); - } - - // Update Redis with the new player - await this.updateGameInRedis(updatedGame, { ...command, playerId: actualPlayerId }); - - const endTime = performance.now(); - logOther('Player joined game successfully', { - gameId: game.id, - gameCode: game.gamecode, - playerId: actualPlayerId, - playerCount: updatedGame.players.length, - maxPlayers: updatedGame.maxplayers, - loginType: game.logintype, - executionTime: Math.round(endTime - startTime) - }); - - return updatedGame; - - } catch (error) { - const endTime = performance.now(); - logError('Failed to join game', error instanceof Error ? error : new Error(String(error))); - logOther('Game join failed', { - gameCode: command.gameCode, - playerId: command.playerId || 'anonymous', - loginType: command.loginType, - executionTime: Math.round(endTime - startTime) - }); - throw error; - } - } - - private validateGameJoinability(game: GameAggregate, playerId: string, command: JoinGameCommand): void { - // Check if game is in waiting state - if (game.state !== GameState.WAITING) { - throw new Error('Game is not accepting new players'); - } - - // Check if player is already in the game - if (game.players.includes(playerId)) { - throw new Error('Player is already in this game'); - } - - // Check if game is full - if (game.players.length >= game.maxplayers) { - throw new Error('Game is full'); - } - - // Note: Login type validation is now handled in the router before reaching this handler - // This ensures proper authentication and organization membership checks are done first - - logOther('Game join validation passed', { - gameId: game.id, - gameCode: game.gamecode, - currentPlayers: game.players.length, - maxPlayers: game.maxplayers, - gameState: game.state, - loginType: game.logintype, - playerId: playerId, - isAuthenticated: !!command.playerId - }); - } - - private async updateGameInRedis(game: GameAggregate, command: JoinGameCommand & { playerId: string }): Promise { - try { - const redisKey = `game:${game.gamecode}`; - - // Get existing game data from Redis or create new - let gameData: ActiveGameData; - const existingData = await this.redisService.get(redisKey); - - if (existingData) { - gameData = JSON.parse(existingData) as ActiveGameData; - } else { - // Create new game data structure - gameData = { - gameId: game.id, - gameCode: game.gamecode, - maxPlayers: game.maxplayers, - currentPlayers: [], - state: game.state, - createdAt: game.createdate, - websocketRoom: `game_${game.gamecode}` - }; - } - - // Add the new player - const newPlayer: GamePlayerData = { - playerId: command.playerId, - playerName: command.playerName, - joinedAt: new Date(), - isOnline: true - }; - - // Check if player name is already in use by a different player - const existingPlayerWithName = gameData.currentPlayers.find( - p => p.playerName === command.playerName && p.playerId !== command.playerId - ); - - if (existingPlayerWithName) { - throw new Error(`Player name "${command.playerName}" is already in use in this game`); - } - - // Update players list (remove if exists, then add) - gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== command.playerId); - gameData.currentPlayers.push(newPlayer); - - // Update game state and player count - gameData.state = game.state; - - // Store updated data in Redis with TTL (24 hours) - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - - logOther('Game data updated in Redis', { - gameId: game.id, - gameCode: game.gamecode, - redisKey, - playerCount: gameData.currentPlayers.length, - websocketRoom: gameData.websocketRoom, - playerId: command.playerId - }); - - } catch (error) { - logError('Failed to update game in Redis', error instanceof Error ? error : new Error(String(error))); - // Don't throw error here - Redis failure shouldn't prevent game join - logOther('Game join completed despite Redis error', { - gameId: game.id, - playerId: command.playerId - }); - } - } - - async getGameFromRedis(gameCode: string): Promise { - try { - const redisKey = `game:${gameCode}`; - const data = await this.redisService.get(redisKey); - return data ? JSON.parse(data) as ActiveGameData : null; - } catch (error) { - logError('Failed to get game from Redis', error instanceof Error ? error : new Error(String(error))); - return null; - } - } - - async removePlayerFromRedis(gameCode: string, playerId: string): Promise { - try { - const redisKey = `game:${gameCode}`; - const existingData = await this.redisService.get(redisKey); - - if (existingData) { - const gameData = JSON.parse(existingData) as ActiveGameData; - gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== playerId); - - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - } - } catch (error) { - logError('Failed to remove player from Redis', error instanceof Error ? error : new Error(String(error))); - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/StartGameCommand.ts b/SerpentRace_Backend/src/Application/Game/commands/StartGameCommand.ts deleted file mode 100644 index e10fad32..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/StartGameCommand.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { LoginType } from '../../../Domain/Game/GameAggregate'; - -export interface StartGameCommand { - deckids: string[]; // Array of deck IDs (3 types, multiple decks per type) - maxplayers: number; // Maximum number of players - logintype: LoginType; // How players can join the game - userid?: string; // Optional user who created the game (becomes game master) - orgid?: string | null; // Organization ID (for organization games) -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/StartGameCommandHandler.ts b/SerpentRace_Backend/src/Application/Game/commands/StartGameCommandHandler.ts deleted file mode 100644 index 931f05fe..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/StartGameCommandHandler.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { StartGameCommand } from './StartGameCommand'; -import { GameAggregate, GameDeck, GameCard, DeckType, GameState } from '../../../Domain/Game/GameAggregate'; -import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; -import { IGameRepository } from '../../../Domain/IRepository/IGameRepository'; -import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; -import { DIContainer } from '../../Services/DIContainer'; -import { RedisService } from '../../Services/RedisService'; -import { logOther, logError } from '../../Services/Logger'; -import { randomBytes } from 'crypto'; -import { GenerateBoardCommand } from './GenerateBoardCommand'; - -export interface ActiveGameData { - gameId: string; - gameCode: string; - hostId?: string; - maxPlayers: number; - currentPlayers: GamePlayerData[]; - state: GameState; - createdAt: Date; - startedAt?: Date; - currentTurn?: string; - websocketRoom: string; -} - -export interface GamePlayerData { - playerId: string; - playerName?: string; - joinedAt: Date; - isOnline: boolean; - position?: number; -} - -export class StartGameCommandHandler { - private gameRepository: IGameRepository; - private deckRepository: IDeckRepository; - private redisService: RedisService; - - constructor() { - this.gameRepository = DIContainer.getInstance().gameRepository; - this.deckRepository = DIContainer.getInstance().deckRepository; - this.redisService = RedisService.getInstance(); - } - - async handle(command: StartGameCommand): Promise { - const startTime = performance.now(); - - try { - logOther('Starting game creation', `deckCount: ${command.deckids.length}, maxPlayers: ${command.maxplayers}, loginType: ${command.logintype}`); - - // Generate unique game code - const gamecode = this.generateGameCode(); - - // Fetch all decks by IDs - const decks = await this.fetchDecks(command.deckids); - - // Validate we have 3 deck types - this.validateDeckTypes(decks); - - // Group decks by type and shuffle cards within each type - const gamedecks = await this.createShuffledGameDecks(decks); - - // Create the game aggregate - const gameData: Partial = { - gamecode, - maxplayers: command.maxplayers, - logintype: command.logintype, - createdby: command.userid!, - orgid: command.orgid || null, - gamedecks, - players: [], - winner: null, - state: GameState.WAITING, - startdate: null, - enddate: null - }; - - // Save the game to database - const savedGame = await this.gameRepository.create(gameData); - - // Create Redis object for real-time game management - await this.createGameInRedis(savedGame, command.userid); - - // Trigger async board generation (don't block game creation) - this.triggerAsyncBoardGeneration(savedGame.id).catch((error: Error) => { - logError('Async board generation failed', error); - }); - - const endTime = performance.now(); - logOther('Game created successfully', `gameId: ${savedGame.id}, gameCode: ${savedGame.gamecode}, executionTime: ${Math.round(endTime - startTime)}ms`); - - return savedGame; - - } catch (error) { - const endTime = performance.now(); - logError('Failed to create game', error instanceof Error ? error : new Error(String(error))); - logOther('Game creation failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - throw new Error('Failed to start game: ' + (error instanceof Error ? error.message : String(error))); - } - } - - private generateGameCode(): string { - // Generate a 6-character alphanumeric game code - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let result = ''; - const randomBytesArray = randomBytes(6); - - for (let i = 0; i < 6; i++) { - result += chars[randomBytesArray[i] % chars.length]; - } - - return result; - } - - private async fetchDecks(deckIds: string[]): Promise { - const decks: DeckAggregate[] = []; - - for (const deckId of deckIds) { - const deck = await this.deckRepository.findById(deckId); - if (!deck) { - throw new Error(`Deck with ID ${deckId} not found`); - } - decks.push(deck); - } - - return decks; - } - - private validateDeckTypes(decks: DeckAggregate[]): void { - const deckTypes = new Set(decks.map(deck => deck.type)); - - // Check if we have all 3 required deck types (LUCK=0, JOKER=1, QUESTION=2) - const requiredTypes = [0, 1, 2]; // Based on Type enum in DeckAggregate - const missingTypes = requiredTypes.filter(type => !deckTypes.has(type)); - - if (missingTypes.length > 0) { - throw new Error(`Missing required deck types: ${missingTypes.join(', ')}. Game requires LUCK, JOKER, and QUESTION deck types.`); - } - - logOther('Deck types validation passed', `foundTypes: [${Array.from(deckTypes).join(', ')}]`); - } - - private async createShuffledGameDecks(decks: DeckAggregate[]): Promise { - // Group decks by type - const decksByType = new Map(); - - decks.forEach(deck => { - if (!decksByType.has(deck.type)) { - decksByType.set(deck.type, []); - } - decksByType.get(deck.type)!.push(deck); - }); - - const gamedecks: GameDeck[] = []; - - // Process each deck type - for (const [deckType, typeDecks] of decksByType) { - // Collect all cards from decks of this type - const allCards: GameCard[] = []; - - typeDecks.forEach(deck => { - deck.cards.forEach(card => { - const gameCard: GameCard = { - cardid: this.generateCardId(), - question: card.text, - answer: card.answer || undefined, - type: card.type, // Include card type for proper processing - consequence: card.consequence || null, - played: false, - playerid: undefined - }; - allCards.push(gameCard); - }); - }); - - // Shuffle all cards of this type - const shuffledCards = this.shuffleArray(allCards); - - // Create game deck for this type - const gameDeck: GameDeck = { - deckid: typeDecks[0].id, // Use first deck ID as representative - decktype: this.mapDeckTypeToGameDeckType(deckType), - cards: shuffledCards - }; - - gamedecks.push(gameDeck); - - logOther('Created shuffled game deck', `type: ${deckType}, cardCount: ${shuffledCards.length}, sourceDecks: ${typeDecks.length}`); - } - - return gamedecks; - } - - private mapDeckTypeToGameDeckType(deckType: number): DeckType { - // Map DeckAggregate.Type to GameAggregate.DeckType - switch (deckType) { - case 0: return DeckType.LUCK; // LUCK = 0 - case 1: return DeckType.JOCKER; // JOKER = 1 - case 2: return DeckType.QUEST; // QUESTION = 2 - default: throw new Error(`Unknown deck type: ${deckType}`); - } - } - - private shuffleArray(array: T[]): T[] { - const shuffled = [...array]; - for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - return shuffled; - } - - private generateCardId(): string { - return randomBytes(8).toString('hex'); - } - - private async createGameInRedis(game: GameAggregate, hostId?: string): Promise { - try { - const redisKey = `game:${game.id}`; - - const gameData: ActiveGameData = { - gameId: game.id, - gameCode: game.gamecode, - hostId: hostId, - maxPlayers: game.maxplayers, - currentPlayers: [], - state: game.state, - createdAt: game.createdate, - websocketRoom: `game_${game.gamecode}` - }; - - // Store game data in Redis with TTL (24 hours) - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - - // Create game room for WebSocket connections - await this.redisService.set(`game_room:${game.gamecode}`, game.id); - - logOther('Game created in Redis', { - gameId: game.id, - gameCode: game.gamecode, - hostId: hostId, - websocketRoom: gameData.websocketRoom, - redisKey - }); - - } catch (error) { - logError('Failed to create game in Redis', error instanceof Error ? error : new Error(String(error))); - // Don't throw error here - Redis failure shouldn't prevent game creation - logOther('Game created successfully despite Redis error', { - gameId: game.id, - gameCode: game.gamecode - }); - } - } - - private async triggerAsyncBoardGeneration(gameId: string): Promise { - try { - // Calculate default field counts based on game configuration - // For now, use reasonable defaults - this should be configurable by host in the future - const maxSpecialFieldsPercentage = parseInt(process.env.MAX_SPECIAL_FIELDS_PERCENTAGE || '67'); - const maxSpecialFields = Math.floor((100 * maxSpecialFieldsPercentage) / 100); - - // Default distribution: 60% positive, 25% negative, 15% luck - const positiveFieldCount = Math.floor(maxSpecialFields * 0.6); - const negativeFieldCount = Math.floor(maxSpecialFields * 0.25); - const luckFieldCount = Math.floor(maxSpecialFields * 0.15); - - const command: GenerateBoardCommand = { - gameId, - positiveFieldCount, - negativeFieldCount, - luckFieldCount - }; - - logOther(`Triggering async board generation for game ${gameId}`, { - positiveFieldCount, - negativeFieldCount, - luckFieldCount, - totalSpecialFields: positiveFieldCount + negativeFieldCount + luckFieldCount - }); - - // Execute board generation in background - await DIContainer.getInstance().generateBoardCommandHandler.execute(command); - - } catch (error) { - logError(`Async board generation failed for game ${gameId}`, error as Error); - // Don't propagate error - board generation failure shouldn't affect game creation - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommand.ts b/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommand.ts deleted file mode 100644 index af62a030..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface StartGamePlayCommand { - gameId: string; // Game ID to start - userId?: string; // User who is starting the game (should be game master) -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommandHandler.ts b/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommandHandler.ts deleted file mode 100644 index 6d352403..00000000 --- a/SerpentRace_Backend/src/Application/Game/commands/StartGamePlayCommandHandler.ts +++ /dev/null @@ -1,480 +0,0 @@ -import { StartGamePlayCommand } from './StartGamePlayCommand'; -import { GameAggregate, GameState, BoardData, GameField } from '../../../Domain/Game/GameAggregate'; -import { IGameRepository } from '../../../Domain/IRepository/IGameRepository'; -import { DIContainer } from '../../Services/DIContainer'; -import { RedisService } from '../../Services/RedisService'; -import { WebSocketService } from '../../Services/WebSocketService'; -import { logOther, logError } from '../../Services/Logger'; - -export interface GamePlayerPosition { - playerId: string; - playerName?: string; - position: number; // Board position (starts at 0) - turnOrder: number; // Random number to determine turn sequence - isOnline: boolean; - joinedAt: Date; -} - -export interface ActiveGamePlayData { - gameId: string; - gameCode: string; - hostId?: string; - maxPlayers: number; - players: GamePlayerPosition[]; - state: GameState; - createdAt: Date; - startedAt: Date; - currentTurn: number; // Index of current player in turn order - currentPlayer: string; // ID of the player whose turn it is - turnSequence: string[]; // Ordered array of player IDs based on turnOrder - websocketRoom: string; - gamePhase: 'starting' | 'playing' | 'paused' | 'finished'; - boardData: BoardData; // Generated board with fields -} - -export interface GameStartResult { - game: GameAggregate; - boardData: BoardData; -} - -export class StartGamePlayCommandHandler { - private gameRepository: IGameRepository; - private redisService: RedisService; - - constructor() { - this.gameRepository = DIContainer.getInstance().gameRepository; - this.redisService = RedisService.getInstance(); - } - - async handle(command: StartGamePlayCommand): Promise { - const startTime = performance.now(); - - try { - logOther('Starting game play', `gameId: ${command.gameId}, userId: ${command.userId || 'system'}`); - - // Find the game - const game = await this.gameRepository.findById(command.gameId); - if (!game) { - throw new Error(`Game with ID ${command.gameId} not found`); - } - - // Validate game can be started - this.validateGameCanStart(game, command.userId); - - // Wait for board generation to complete (max 20 seconds) - const boardData = await this.waitForBoardGeneration(game.id); - - // Update game state in database - const updatedGame = await this.gameRepository.update(game.id, { - state: GameState.ACTIVE, - startdate: new Date() - }); - - if (!updatedGame) { - throw new Error('Failed to update game state'); - } - - // Initialize game play in Redis with board data - await this.initializeGamePlayInRedis(updatedGame, boardData); - - // Notify all players via WebSocket - await this.notifyGameStart(updatedGame); - - const endTime = performance.now(); - logOther('Game play started successfully', { - gameId: updatedGame.id, - gameCode: updatedGame.gamecode, - playerCount: updatedGame.players.length, - executionTime: Math.round(endTime - startTime) - }); - - return { - game: updatedGame, - boardData: boardData - }; - - } catch (error) { - const endTime = performance.now(); - logError('Failed to start game play', error instanceof Error ? error : new Error(String(error))); - logOther('Game start failed', { - gameId: command.gameId, - userId: command.userId, - executionTime: Math.round(endTime - startTime) - }); - throw error; - } - } - - private validateGameCanStart(game: GameAggregate, userId?: string): void { - // Check if game is in waiting state - if (game.state !== GameState.WAITING) { - throw new Error('Game is not in waiting state and cannot be started'); - } - - // Check if there are enough players (at least 2) - if (game.players.length < 2) { - throw new Error('Game needs at least 2 players to start'); - } - - // For private and organization games, check if user is game master - if (game.createdby && userId && game.createdby !== userId) { - throw new Error('Only the game master can start this game'); - } - - logOther('Game start validation passed', { - gameId: game.id, - gameCode: game.gamecode, - playerCount: game.players.length, - gameState: game.state, - isGameMaster: !game.createdby || (userId && game.createdby === userId) - }); - } - - private async initializeGamePlayInRedis(game: GameAggregate, boardData: BoardData): Promise { - try { - const redisKey = `gameplay:${game.gamecode}`; - - // Get connected player names from Redis (stored by WebSocket) - const playerNamesMap = await this.getPlayerNames(game.gamecode); - - // Generate random turn orders for all players - const playersWithPositions = this.initializePlayerPositions(game.players, playerNamesMap); - - // Sort by turn order to create turn sequence - const turnSequence = [...playersWithPositions] - .sort((a, b) => a.turnOrder - b.turnOrder) - .map(p => p.playerId); - - const gamePlayData: ActiveGamePlayData = { - gameId: game.id, - gameCode: game.gamecode, - hostId: game.createdby || undefined, - maxPlayers: game.maxplayers, - players: playersWithPositions, - state: GameState.ACTIVE, - createdAt: game.createdate, - startedAt: new Date(), - currentTurn: 0, // Start with first player in sequence - currentPlayer: turnSequence[0], // First player in turn sequence - turnSequence, - websocketRoom: `game_${game.gamecode}`, - gamePhase: 'starting', - boardData - }; - - // Store game play data in Redis with TTL (24 hours) - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gamePlayData), 24 * 60 * 60); - - logOther('Game play initialized in Redis', { - gameId: game.id, - gameCode: game.gamecode, - playerCount: playersWithPositions.length, - turnSequence, - currentPlayer: turnSequence[0], - redisKey - }); - - } catch (error) { - logError('Failed to initialize game play in Redis', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to initialize game session'); - } - } - - private initializePlayerPositions(playerIds: string[], playerNamesMap: Map): GamePlayerPosition[] { - const players: GamePlayerPosition[] = []; - - // Generate random turn orders (1 to playerCount) - const turnOrders = this.generateRandomTurnOrders(playerIds.length); - - playerIds.forEach((playerId, index) => { - players.push({ - playerId, - playerName: playerNamesMap.get(playerId) || playerId, // Use mapped name or fallback to ID - position: 0, // All players start at position 0 - turnOrder: turnOrders[index], - isOnline: true, // Assume online when game starts - joinedAt: new Date() - }); - }); - - logOther('Player positions initialized', { - playerCount: players.length, - turnOrders: turnOrders, - playersData: players.map(p => ({ - playerId: p.playerId, - playerName: p.playerName, - position: p.position, - turnOrder: p.turnOrder - })) - }); - - return players; - } - - private generateRandomTurnOrders(playerCount: number): number[] { - // Create array [1, 2, 3, ..., playerCount] - const orders = Array.from({ length: playerCount }, (_, i) => i + 1); - - // Fisher-Yates shuffle - for (let i = orders.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [orders[i], orders[j]] = [orders[j], orders[i]]; - } - - return orders; - } - - private async notifyGameStart(game: GameAggregate): Promise { - try { - // Get game play data from Redis (contains board data) - const gamePlayData = await this.getGamePlayFromRedis(game.gamecode); - if (!gamePlayData) { - logError('Game play data not found in Redis', new Error('Missing game play data')); - return; - } - - const boardData = gamePlayData.boardData; - if (!boardData) { - logError('Board data not found in game play data', new Error('Missing board data')); - return; - } - - // Get WebSocket service from DIContainer and broadcast game start - const gameWebSocketService = DIContainer.getInstance().gameWebSocketService; - await gameWebSocketService.broadcastGameStart( - game.gamecode, - boardData, - gamePlayData.turnSequence, - game - ); - - logOther('Game start notifications sent via WebSocket', { - gameId: game.id, - gameCode: game.gamecode, - playerCount: game.players.length, - websocketRoom: `game_${game.gamecode}`, - firstPlayer: gamePlayData.turnSequence[0] - }); - - } catch (error) { - logError('Failed to send game start notifications', error instanceof Error ? error : new Error(String(error))); - // Don't throw error here - notification failure shouldn't prevent game start - } - } - - async getGamePlayFromRedis(gameCode: string): Promise { - try { - const redisKey = `gameplay:${gameCode}`; - const data = await this.redisService.get(redisKey); - return data ? JSON.parse(data) as ActiveGamePlayData : null; - } catch (error) { - logError('Failed to get game play from Redis', error instanceof Error ? error : new Error(String(error))); - return null; - } - } - - async updatePlayerPosition(gameCode: string, playerId: string, newPosition: number): Promise { - try { - const gameData = await this.getGamePlayFromRedis(gameCode); - if (!gameData) { - throw new Error('Game session not found'); - } - - // Update player position - const player = gameData.players.find(p => p.playerId === playerId); - if (player) { - player.position = newPosition; - - // Save back to Redis - const redisKey = `gameplay:${gameCode}`; - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - - logOther('Player position updated', { - gameCode, - playerId, - newPosition - }); - } - } catch (error) { - logError('Failed to update player position', error instanceof Error ? error : new Error(String(error))); - throw error; - } - } - - async getNextPlayer(gameCode: string): Promise { - try { - const gameData = await this.getGamePlayFromRedis(gameCode); - if (!gameData) { - return null; - } - - const nextTurnIndex = (gameData.currentTurn + 1) % gameData.turnSequence.length; - return gameData.turnSequence[nextTurnIndex]; - } catch (error) { - logError('Failed to get next player', error instanceof Error ? error : new Error(String(error))); - return null; - } - } - - private async getPlayerNames(gameCode: string): Promise> { - try { - // Get active game data from Redis which contains player names - const activeGameKey = `game:${gameCode}`; - const activeGameStr = await this.redisService.get(activeGameKey); - - const playerNamesMap = new Map(); - - if (activeGameStr) { - const activeGame = JSON.parse(activeGameStr); - if (activeGame.currentPlayers && Array.isArray(activeGame.currentPlayers)) { - // Map playerIds to playerNames from active game data - activeGame.currentPlayers.forEach((player: any) => { - if (player.playerId && player.playerName) { - playerNamesMap.set(player.playerId, player.playerName); - } - }); - } - } - - logOther('Retrieved player names map', { - gameCode, - playerCount: playerNamesMap.size, - players: Array.from(playerNamesMap.entries()).map(([id, name]) => ({ id, name })) - }); - - return playerNamesMap; - } catch (error) { - logError('Failed to get player names', error instanceof Error ? error : new Error(String(error))); - return new Map(); - } - } - - async advanceTurn(gameId: string): Promise { - try { - const gameData = await this.getGamePlayFromRedis(gameId); - if (!gameData) { - return null; - } - - // Advance to next player - gameData.currentTurn = (gameData.currentTurn + 1) % gameData.turnSequence.length; - const currentPlayer = gameData.turnSequence[gameData.currentTurn]; - - // Save back to Redis - const redisKey = `gameplay:${gameId}`; - await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60); - - logOther('Turn advanced', { - gameId, - currentTurn: gameData.currentTurn, - currentPlayer - }); - - return currentPlayer; - } catch (error) { - logError('Failed to advance turn', error instanceof Error ? error : new Error(String(error))); - return null; - } - } - - private async waitForBoardGeneration(gameId: string): Promise { - const maxWaitTime = parseInt(process.env.MAX_GENERATION_TIME_SECONDS || '20') * 1000; - const pollInterval = 500; // Check every 500ms - const startTime = Date.now(); - - logOther(`Waiting for board generation for game ${gameId}`, { - maxWaitTime: maxWaitTime / 1000, - pollInterval, - redisKey: `game_board_${gameId}` - }); - - while (Date.now() - startTime < maxWaitTime) { - try { - const redisKey = `game_board_${gameId}`; - const boardDataStr = await this.redisService.get(redisKey); - - logOther(`Board generation check for game ${gameId}`, { - attempt: Math.floor((Date.now() - startTime) / pollInterval) + 1, - hasData: !!boardDataStr, - dataLength: boardDataStr ? boardDataStr.length : 0, - waitTime: Date.now() - startTime - }); - - if (boardDataStr) { - const boardData: BoardData = JSON.parse(boardDataStr); - - logOther(`Board data found for game ${gameId}`, { - generationComplete: boardData.generationComplete, - hasError: !!boardData.error, - fieldsCount: boardData.fields ? boardData.fields.length : 0 - }); - - if (boardData.generationComplete) { - if (boardData.error) { - logError(`Board generation failed for game ${gameId}`, new Error(boardData.error)); - throw new Error(`Board generation failed: ${boardData.error}`); - } - - logOther(`Board generation completed for game ${gameId}`, { - fieldCount: boardData.fields.length, - waitTime: Date.now() - startTime - }); - - return boardData; - } - } else { - // No board data found yet - check if we need to trigger generation - logOther(`No board data found yet for game ${gameId}, checking if generation was triggered...`, { - waitTime: Date.now() - startTime, - redisKey - }); - - // If we've waited for 2 seconds and still no data, try to trigger generation manually - if (Date.now() - startTime > 2000) { - await this.ensureBoardGenerationTriggered(gameId); - } - } - - // Wait before next poll - await new Promise(resolve => setTimeout(resolve, pollInterval)); - - } catch (error) { - logError(`Error checking board generation status for game ${gameId}`, error as Error); - throw new Error(`Failed to retrieve board data: ${error instanceof Error ? error.message : String(error)}`); - } - } - - // Timeout reached - logError(`Board generation timeout for game ${gameId}`, new Error(`Generation took longer than ${maxWaitTime / 1000} seconds`)); - throw new Error(`Board generation timeout. Game ${gameId} is not ready to start. Please try again later.`); - } - - private async ensureBoardGenerationTriggered(gameId: string): Promise { - try { - logOther(`Ensuring board generation is triggered for game ${gameId}`); - - // Check if generation was already triggered by looking for any board data - const redisKey = `game_board_${gameId}`; - const existingData = await this.redisService.get(redisKey); - - if (!existingData) { - // No data at all - trigger generation manually - logOther(`No board generation found for game ${gameId}, triggering manually`); - - // Use DIContainer to trigger board generation - const generateBoardCommand = { - gameId, - positiveFieldCount: Math.floor(67 * 0.6), // Default: 60% positive - negativeFieldCount: Math.floor(67 * 0.25), // Default: 25% negative - luckFieldCount: Math.floor(67 * 0.15) // Default: 15% luck - }; - - await DIContainer.getInstance().generateBoardCommandHandler.execute(generateBoardCommand); - logOther(`Board generation manually triggered for game ${gameId}`); - } - } catch (error) { - logError(`Failed to ensure board generation for game ${gameId}`, error as Error); - // Don't throw here - let the main wait loop handle the timeout - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommand.ts b/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommand.ts deleted file mode 100644 index 8e0f95fc..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommand.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface CreateOrganizationCommand { - name: string; - contactfname: string; - contactlname: string; - contactphone: string; - contactemail: string; - url?: string; -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommandHandler.ts b/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommandHandler.ts deleted file mode 100644 index d77ed9de..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/CreateOrganizationCommandHandler.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { CreateOrganizationCommand } from './CreateOrganizationCommand'; -import { ShortOrganizationDto } from '../../DTOs/OrganizationDto'; -import { OrganizationAggregate, OrganizationState } from '../../../Domain/Organization/OrganizationAggregate'; -import { OrganizationMapper } from '../../DTOs/Mappers/OrganizationMapper'; - -export class CreateOrganizationCommandHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(cmd: CreateOrganizationCommand): Promise { - try { - const org = new OrganizationAggregate(); - org.name = cmd.name; - org.contactfname = cmd.contactfname; - org.contactlname = cmd.contactlname; - org.contactphone = cmd.contactphone; - org.contactemail = cmd.contactemail; - org.url = cmd.url || null; - org.state = OrganizationState.REGISTERED; - - const created = await this.orgRepo.create(org); - return OrganizationMapper.toShortDto(created); - } catch (error) { - if (error instanceof Error) { - if (error.message.includes('duplicate key value violates unique constraint')) { - throw new Error('Organization with this name or contact email already exists'); - } - } - throw new Error('Failed to create organization'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommand.ts b/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommand.ts deleted file mode 100644 index 60a31806..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface DeleteOrganizationCommand { - id: string; - soft?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommandHandler.ts b/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommandHandler.ts deleted file mode 100644 index a9e1b965..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/DeleteOrganizationCommandHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { DeleteOrganizationCommand } from './DeleteOrganizationCommand'; - - -export class DeleteOrganizationCommandHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(cmd: DeleteOrganizationCommand): Promise { - if (cmd.soft) { - await this.orgRepo.softDelete(cmd.id); - } else { - await this.orgRepo.delete(cmd.id); - } - return true; - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommand.ts b/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommand.ts deleted file mode 100644 index 6fa97b73..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommand.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ProcessOrgAuthCallbackCommand { - organizationId: string; - userId: string; - status: 'ok' | 'not_ok'; - authToken?: string; -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommandHandler.ts b/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommandHandler.ts deleted file mode 100644 index 39a41abe..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/ProcessOrgAuthCallbackCommandHandler.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { ProcessOrgAuthCallbackCommand } from './ProcessOrgAuthCallbackCommand'; -import { logAuth, logDatabase, logError, logWarning } from '../../Services/Logger'; - -export interface ProcessOrgAuthCallbackResponse { - success: boolean; - message: string; - updatedFields?: string[]; -} - -export class ProcessOrgAuthCallbackCommandHandler { - constructor( - private readonly userRepo: IUserRepository, - private readonly orgRepo: IOrganizationRepository - ) {} - - async execute(cmd: ProcessOrgAuthCallbackCommand): Promise { - const startTime = Date.now(); - - try { - logAuth('Processing organization authentication callback', cmd.userId, { - organizationId: cmd.organizationId, - status: cmd.status, - hasAuthToken: !!cmd.authToken - }); - - // Verify organization exists - const organization = await this.orgRepo.findById(cmd.organizationId); - if (!organization) { - logWarning('Organization not found for auth callback', { - organizationId: cmd.organizationId, - userId: cmd.userId - }); - return { - success: false, - message: 'Organization not found' - }; - } - - // Verify user exists - const user = await this.userRepo.findById(cmd.userId); - if (!user) { - logWarning('User not found for auth callback', { - organizationId: cmd.organizationId, - userId: cmd.userId - }); - return { - success: false, - message: 'User not found' - }; - } - - // Verify user belongs to the organization - if (user.orgid !== cmd.organizationId) { - logWarning('User does not belong to organization for auth callback', { - organizationId: cmd.organizationId, - userId: cmd.userId, - userOrgId: user.orgid - }); - return { - success: false, - message: 'User does not belong to this organization' - }; - } - - if (cmd.status === 'not_ok') { - logAuth('Organization authentication failed', cmd.userId, { - organizationId: cmd.organizationId, - organizationName: organization.name - }); - return { - success: false, - message: 'Organization authentication failed' - }; - } - - // Update user's organization login date - const now = new Date(); - const updatedUser = await this.userRepo.update(cmd.userId, { - Orglogindate: now - }); - - if (!updatedUser) { - logError('Failed to update user organization login date', new Error('User update returned null')); - return { - success: false, - message: 'Failed to update user login information' - }; - } - - logAuth('Organization authentication successful', cmd.userId, { - organizationId: cmd.organizationId, - organizationName: organization.name, - orgLoginDate: now.toISOString(), - executionTime: Date.now() - startTime - }); - - logDatabase('User organization login date updated', - `userId: ${cmd.userId}, orgId: ${cmd.organizationId}`, - Date.now() - startTime, - { - userId: cmd.userId, - organizationId: cmd.organizationId, - newOrgLoginDate: now.toISOString() - } - ); - - return { - success: true, - message: 'Organization authentication successful', - updatedFields: ['Orglogindate'] - }; - - } catch (error) { - logError('ProcessOrgAuthCallbackCommandHandler error', error as Error); - return { - success: false, - message: 'Internal error processing authentication callback' - }; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommand.ts b/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommand.ts deleted file mode 100644 index 040f893d..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommand.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { OrganizationStateType } from '../../../Domain/Organization/OrganizationAggregate'; - -export interface UpdateOrganizationCommand { - id: string; - name?: string; - contactfname?: string; - contactlname?: string; - contactphone?: string; - contactemail?: string; - url?: string; - state?: OrganizationStateType; - userinorg?: number; - maxOrganizationalDecks?: number | null; -} diff --git a/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommandHandler.ts b/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommandHandler.ts deleted file mode 100644 index 32a145a8..00000000 --- a/SerpentRace_Backend/src/Application/Organization/commands/UpdateOrganizationCommandHandler.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { UpdateOrganizationCommand } from './UpdateOrganizationCommand'; - -import { ShortOrganizationDto } from '../../DTOs/OrganizationDto'; -import { OrganizationMapper } from '../../DTOs/Mappers/OrganizationMapper'; - -export class UpdateOrganizationCommandHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(cmd: UpdateOrganizationCommand): Promise { - const updated = await this.orgRepo.update(cmd.id, { ...cmd }); - if (!updated) return null; - return OrganizationMapper.toShortDto(updated); - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQuery.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQuery.ts deleted file mode 100644 index e8473c15..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetOrganizationByIdQuery { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQueryHandler.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQueryHandler.ts deleted file mode 100644 index 60c82768..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationByIdQueryHandler.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { GetOrganizationByIdQuery } from './GetOrganizationByIdQuery'; - -import { ShortOrganizationDto } from '../../DTOs/OrganizationDto'; -import { OrganizationMapper } from '../../DTOs/Mappers/OrganizationMapper'; - -export class GetOrganizationByIdQueryHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(query: GetOrganizationByIdQuery): Promise { - const org = await this.orgRepo.findById(query.id); - if (!org) return null; - return OrganizationMapper.toShortDto(org); - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQuery.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQuery.ts deleted file mode 100644 index 26370b47..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetOrganizationLoginUrlQuery { - organizationId: string; -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQueryHandler.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQueryHandler.ts deleted file mode 100644 index f7de9707..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationLoginUrlQueryHandler.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { GetOrganizationLoginUrlQuery } from './GetOrganizationLoginUrlQuery'; -import { OrganizationLoginUrlDto } from '../../DTOs/OrganizationDto'; -import { logDatabase, logError, logWarning } from '../../Services/Logger'; - -export class GetOrganizationLoginUrlQueryHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(query: GetOrganizationLoginUrlQuery): Promise { - const startTime = Date.now(); - - try { - logDatabase('Getting organization login URL', `organizationId: ${query.organizationId}`, 0, { - organizationId: query.organizationId - }); - - const organization = await this.orgRepo.findById(query.organizationId); - - if (!organization) { - logWarning('Organization not found for login URL request', { - organizationId: query.organizationId - }); - return null; - } - - if (!organization.url) { - logWarning('Organization has no configured login URL', { - organizationId: query.organizationId, - organizationName: organization.name - }); - return null; - } - - const result: OrganizationLoginUrlDto = { - organizationId: organization.id, - organizationName: organization.name, - loginUrl: organization.url - }; - - logDatabase('Organization login URL retrieved successfully', - `organizationId: ${query.organizationId}`, - Date.now() - startTime, - { - organizationId: organization.id, - organizationName: organization.name, - hasUrl: !!organization.url - } - ); - - return result; - } catch (error) { - logError('GetOrganizationLoginUrlQueryHandler error', error as Error); - throw new Error('Failed to retrieve organization login URL'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQuery.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQuery.ts deleted file mode 100644 index ec22eb96..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQuery.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GetOrganizationsByPageQuery { - from: number; - to: number; - includeDeleted?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQueryHandler.ts deleted file mode 100644 index b838d81f..00000000 --- a/SerpentRace_Backend/src/Application/Organization/queries/GetOrganizationsByPageQueryHandler.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { GetOrganizationsByPageQuery } from './GetOrganizationsByPageQuery'; -import { ShortOrganizationDto } from '../../DTOs/OrganizationDto'; -import { OrganizationMapper } from '../../DTOs/Mappers/OrganizationMapper'; -import { logError, logRequest } from '../../Services/Logger'; - -export class GetOrganizationsByPageQueryHandler { - constructor(private readonly orgRepo: IOrganizationRepository) {} - - async execute(query: GetOrganizationsByPageQuery): Promise<{ organizations: ShortOrganizationDto[], totalCount: number }> { - try { - // Validate pagination parameters - if (query.from < 0 || query.to < query.from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = query.to - query.from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - logRequest('Get organizations by page query started', undefined, undefined, { - from: query.from, - to: query.to, - includeDeleted: query.includeDeleted || false - }); - - const result = query.includeDeleted - ? await this.orgRepo.findByPageIncludingDeleted(query.from, query.to) - : await this.orgRepo.findByPage(query.from, query.to); - - logRequest('Get organizations by page query completed', undefined, undefined, { - from: query.from, - to: query.to, - returned: result.organizations.length, - totalCount: result.totalCount, - includeDeleted: query.includeDeleted || false - }); - - return { - organizations: OrganizationMapper.toShortDtoList(result.organizations), - totalCount: result.totalCount - }; - } catch (error) { - logError('GetOrganizationsByPageQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Handle database errors - if (error instanceof Error && error.message.includes('database')) { - throw new Error('Database connection error'); - } - - // Re-throw validation errors as-is - if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) { - throw error; - } - - throw new Error('Failed to retrieve organizations'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Search/Generalsearch.ts b/SerpentRace_Backend/src/Application/Search/Generalsearch.ts deleted file mode 100644 index 0d940191..00000000 --- a/SerpentRace_Backend/src/Application/Search/Generalsearch.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { IUserRepository } from '../../Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../Domain/IRepository/IOrganizationRepository'; -import { IDeckRepository } from '../../Domain/IRepository/IDeckRepository'; -import { SearchQuery, SearchResult } from '../DTOs/SearchDto'; -import { ShortUserDto, DetailUserDto } from '../DTOs/UserDto'; -import { ShortOrganizationDto, DetailOrganizationDto } from '../DTOs/OrganizationDto'; -import { ShortDeckDto, DetailDeckDto } from '../DTOs/DeckDto'; -import { UserMapper } from '../DTOs/Mappers/UserMapper'; -import { OrganizationMapper } from '../DTOs/Mappers/OrganizationMapper'; -import { DeckMapper } from '../DTOs/Mappers/DeckMapper'; - -export type SearchType = 'users' | 'organizations' | 'decks'; - -export interface IGeneralSearchService { - searchUsers(searchQuery: SearchQuery): Promise>; - searchOrganizations(searchQuery: SearchQuery): Promise>; - searchDecks(searchQuery: SearchQuery): Promise>; - searchByType(searchType: SearchType, searchQuery: SearchQuery): Promise>; -} - -export class GeneralSearchService implements IGeneralSearchService { - constructor( - private userRepo: IUserRepository, - private organizationRepo: IOrganizationRepository, - private deckRepo: IDeckRepository - ) {} - - static getSearchTypeFromUrl(url: string): SearchType { - if (url.includes('/users/') || url.includes('/api/users/')) { - return 'users'; - } else if (url.includes('/organizations/') || url.includes('/api/organizations/')) { - return 'organizations'; - } else if (url.includes('/decks/') || url.includes('/api/decks/')) { - return 'decks'; - } - return 'users'; - } - - async searchUsers(searchQuery: SearchQuery): Promise> { - const { query, limit = 20, offset = 0 } = searchQuery; - - if (!query || query.trim().length === 0) { - return { - results: [], - totalCount: 0, - hasMore: false, - searchQuery: query, - searchType: 'users' - }; - } - - // Ensure limit is at least 1 to prevent database issues - const effectiveLimit = Math.max(limit || 20, 1); - const effectiveOffset = Math.max(offset || 0, 0); - - try { - const { users, totalCount } = await this.userRepo.search(query.trim(), effectiveLimit, effectiveOffset); - const results = users.map(user => UserMapper.toShortDto(user)); - const hasMore = (effectiveOffset + effectiveLimit) < totalCount; - - return { - results, - totalCount, - hasMore, - searchQuery: query, - searchType: 'users' - }; - } catch (error) { - throw new Error('Failed to search users'); - } - } - - async searchOrganizations(searchQuery: SearchQuery): Promise> { - const { query, limit = 20, offset = 0 } = searchQuery; - - if (!query || query.trim().length === 0) { - return { - results: [], - totalCount: 0, - hasMore: false, - searchQuery: query, - searchType: 'organizations' - }; - } - - const { organizations, totalCount } = await this.organizationRepo.search(query.trim(), limit, offset); - const results = organizations.map(org => OrganizationMapper.toShortDto(org)); - const hasMore = (offset + limit) < totalCount; - - return { - results, - totalCount, - hasMore, - searchQuery: query, - searchType: 'organizations' - }; - } - - async searchDecks(searchQuery: SearchQuery): Promise> { - const { query, limit = 20, offset = 0 } = searchQuery; - - if (!query || query.trim().length === 0) { - return { - results: [], - totalCount: 0, - hasMore: false, - searchQuery: query, - searchType: 'decks' - }; - } - - // Ensure limit is at least 1 to prevent database issues - const effectiveLimit = Math.max(limit || 20, 1); - const effectiveOffset = Math.max(offset || 0, 0); - - try { - const { decks, totalCount } = await this.deckRepo.search(query.trim(), effectiveLimit, effectiveOffset); - const results = decks.map(deck => DeckMapper.toShortDto(deck)); - const hasMore = (effectiveOffset + effectiveLimit) < totalCount; - - return { - results, - totalCount, - hasMore, - searchQuery: query, - searchType: 'decks' - }; - } catch (error) { - throw new Error('Failed to search decks'); - } - } - - async searchByType( - searchType: SearchType, - searchQuery: SearchQuery - ): Promise> { - switch (searchType) { - case 'users': - return await this.searchUsers(searchQuery) as SearchResult; - case 'organizations': - return await this.searchOrganizations(searchQuery) as SearchResult; - case 'decks': - return await this.searchDecks(searchQuery) as SearchResult; - default: - throw new Error(`Unsupported search type: ${searchType}`); - } - } - - async searchFromUrl( - url: string, - searchQuery: SearchQuery - ): Promise> { - const searchType = GeneralSearchService.getSearchTypeFromUrl(url); - return await this.searchByType(searchType, searchQuery); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/AdminBypassService.ts b/SerpentRace_Backend/src/Application/Services/AdminBypassService.ts deleted file mode 100644 index 6732c15f..00000000 --- a/SerpentRace_Backend/src/Application/Services/AdminBypassService.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { UserState } from '../../Domain/User/UserAggregate'; -import { logAuth } from './Logger'; -import { Request, Response } from 'express'; - -/** - * Admin Bypass Service - Centralized admin privilege checking and logging - */ -export class AdminBypassService { - /** - * Check if user has admin privileges - * @param userState - User's current state - * @returns true if user is admin - */ - static isAdmin(userState: UserState): boolean { - return userState === UserState.ADMIN; - } - - /** - * Check if user should bypass all restrictions - * @param userState - User's current state - * @returns true if restrictions should be bypassed - */ - static shouldBypassRestrictions(userState: UserState): boolean { - return this.isAdmin(userState); - } - - /** - * Log admin bypass action for audit trail - * @param action - Description of the action being bypassed - * @param adminUserId - ID of the admin user - * @param targetId - ID of the target resource - * @param details - Additional details about the bypass - * @param req - Optional request object for context - * @param res - Optional response object for context - */ - static logAdminBypass( - action: string, - adminUserId: string, - targetId: string, - details?: any, - req?: Request, - res?: Response - ): void { - logAuth(`ADMIN_BYPASS: ${action}`, adminUserId, { - targetId, - action, - bypassReason: 'Admin privileges', - timestamp: new Date().toISOString(), - ...details - }, req, res); - } -} - -/** - * Admin Audit Service - Enhanced logging for all admin actions - */ -export class AdminAuditService { - /** - * Log comprehensive admin action for audit trail - * @param action - Action being performed - * @param adminUserId - ID of the admin user - * @param details - Detailed information about the action - * @param req - Request object for context - * @param res - Response object for context - */ - static logAdminAction( - action: string, - adminUserId: string, - details: { - targetType: 'user' | 'organization' | 'deck' | 'contact' | 'chat'; - targetId: string; - operation: 'create' | 'read' | 'update' | 'delete' | 'bypass' | 'export' | 'import'; - changes?: any; - sensitive?: boolean; - metadata?: any; - }, - req?: Request, - res?: Response - ): void { - - const auditData = { - timestamp: new Date().toISOString(), - adminUserId, - action, - ...details, - ip: req?.ip, - userAgent: req?.get('User-Agent'), - endpoint: req?.path, - method: req?.method, - requestId: req?.headers['x-request-id'] || 'unknown' - }; - - // Enhanced logging for admin actions - logAuth(`ADMIN_AUDIT: ${action}`, adminUserId, auditData, req, res); - - // Additional security logging for sensitive operations - if (details.sensitive) { - logAuth(`ADMIN_SENSITIVE: ${action}`, adminUserId, { - ...auditData, - alertLevel: 'HIGH', - requiresReview: true - }, req, res); - } - } - - /** - * Log bulk admin operations - * @param action - Bulk action being performed - * @param adminUserId - ID of the admin user - * @param affectedCount - Number of resources affected - * @param targetType - Type of resources affected - * @param req - Request object for context - * @param res - Response object for context - */ - static logBulkAdminAction( - action: string, - adminUserId: string, - affectedCount: number, - targetType: string, - req?: Request, - res?: Response - ): void { - this.logAdminAction(`BULK_${action}`, adminUserId, { - targetType: targetType as any, - targetId: `bulk-${affectedCount}-items`, - operation: 'update' as any, - metadata: { affectedCount }, - sensitive: affectedCount > 10 // Mark large bulk operations as sensitive - }, req, res); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/AuthMiddleware.ts b/SerpentRace_Backend/src/Application/Services/AuthMiddleware.ts deleted file mode 100644 index 46496685..00000000 --- a/SerpentRace_Backend/src/Application/Services/AuthMiddleware.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { JWTService } from './JWTService'; -import { RedisService } from './RedisService'; -import { logAuth, logWarning } from './Logger'; - -export const jwtService = new JWTService(); -const redisService = RedisService.getInstance(); - -/** - * Check if a token is blacklisted - */ -async function isTokenBlacklisted(token: string): Promise { - try { - const result = await redisService.get(`blacklist:${token}`); - return result === 'true'; - } catch (error) { - // If Redis is down, allow the request to proceed (fail open) - logWarning('Failed to check token blacklist - allowing request', { error: (error as Error).message }); - return false; - } -} - -/** - * Extract token from request (cookie or Authorization header) - */ -function extractToken(req: Request, type: 'auth' | 'refresh'): string | null { - // First try to get token from cookie - const cookieToken = req.cookies[`${type}_token`]; - if (cookieToken) { - return cookieToken; - } - - // Fallback to Authorization header - const authHeader = req.headers.authorization; - if (authHeader && authHeader.startsWith('Bearer ')) { - return authHeader.substring(7); - } - - return null; -} - -export async function authRequired(req: Request, res: Response, next: NextFunction) { - try { - // Extract token from request - const token = extractToken(req, "auth"); - const refreshToken = extractToken(req, "refresh"); - if (!token || !refreshToken) { - logAuth('Authentication failed - No token provided', undefined, { - ip: req.ip, - userAgent: req.get ? req.get('User-Agent') : 'unknown', - path: req.path - }, req); - return res.status(401).json({ error: 'Unauthorized' }); - } - - // Check if token is blacklisted - const isBlacklisted = await isTokenBlacklisted(token); - if (isBlacklisted) { - logAuth('Authentication failed - Token blacklisted', undefined, { - ip: req.ip, - userAgent: req.get ? req.get('User-Agent') : 'unknown', - path: req.path - }, req); - return res.status(401).json({ error: 'Token has been invalidated' }); - } - - // Verify token - const payload = jwtService.verify(req); - if (!payload) { - logAuth('Authentication failed - Invalid token', undefined, { - ip: req.ip, - userAgent: req.get ? req.get('User-Agent') : 'unknown', - path: req.path - }, req); - return res.status(401).json({ error: 'Unauthorized' }); - } - - logAuth('Authentication successful', payload.userId, { - authLevel: payload.authLevel, - orgId: payload.orgId - }, req); - - const refreshed = jwtService.refreshIfNeeded(payload, res, req); - if (refreshed) { - logAuth('Token refreshed', payload.userId, undefined, req); - } - - (req as any).user = payload; - next(); - } catch (error) { - logWarning('Authentication middleware error', { error: (error as Error).message }, req); - return res.status(500).json({ error: 'Internal server error' }); - } -} - -export async function adminRequired(req: Request, res: Response, next: NextFunction) { - try { - // Extract token from request - const token = extractToken(req, "auth"); - const refreshToken = extractToken(req, "refresh"); - if (!token || !refreshToken) { - logWarning('Admin access denied - No token provided', { - ip: req.ip, - path: req.path - }, req); - return res.status(401).json({ error: 'Unauthorized' }); - } - - // Check if token is blacklisted - const isBlacklisted = await isTokenBlacklisted(token); - if (isBlacklisted) { - logWarning('Admin access denied - Token blacklisted', { - ip: req.ip, - path: req.path - }, req); - return res.status(401).json({ error: 'Token has been invalidated' }); - } - - // Verify token and check admin privileges - const payload = jwtService.verify(req); - if (!payload || payload.authLevel !== 1) { - logWarning('Admin access denied', { - hasPayload: !!payload, - authLevel: payload?.authLevel, - userId: payload?.userId, - ip: req.ip, - path: req.path - }, req); - return res.status(403).json({ error: 'Forbidden' }); - } - - logAuth('Admin authentication successful', payload.userId, { - authLevel: payload.authLevel, - orgId: payload.orgId - }, req); - - const refreshed = jwtService.refreshIfNeeded(payload, res, req); - if (refreshed) { - logAuth('Admin token refreshed', payload.userId, undefined, req); - } - - (req as any).user = payload; - next(); - } catch (error) { - logWarning('Admin authentication middleware error', { error: (error as Error).message }, req); - return res.status(500).json({ error: 'Internal server error' }); - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/CardDrawingService.ts b/SerpentRace_Backend/src/Application/Services/CardDrawingService.ts deleted file mode 100644 index cbb0d3bf..00000000 --- a/SerpentRace_Backend/src/Application/Services/CardDrawingService.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { GameAggregate, GameCard, DeckType, GameDeck } from '../../Domain/Game/GameAggregate'; -import { ConsequenceType } from '../../Domain/Deck/DeckAggregate'; -import { CardProcessingService, CardClientData, CardValidationResult } from './CardProcessingService'; - -export interface CardDrawResult { - success: boolean; - card?: GameCard; - clientData?: CardClientData; // Prepared data for client - error?: string; -} - -export interface CardAnswerResult { - correct: boolean; - consequence: ConsequenceType; - description: string; - validationDetails?: CardValidationResult; // Detailed validation info -} - -export interface PendingCardAnswer { - gameId: string; - playerId: string; - card: GameCard; - timeoutId: NodeJS.Timeout; - startTime: Date; -} - -/** - * Service responsible for handling card drawing mechanics during special field landings - * Integrates with existing GameCard interface and DeckType enum - */ -export class CardDrawingService { - private pendingAnswers: Map = new Map(); - private readonly ANSWER_TIMEOUT_MS = 60000; // 1 minute - private cardProcessingService: CardProcessingService; - - constructor() { - this.cardProcessingService = new CardProcessingService(); - } - - /** - * Draw a card from the appropriate deck based on field type - * @param game Game aggregate containing the deck information - * @param fieldType Type of field the player landed on - * @param playerId ID of the player who needs to draw the card - * @returns Card draw result with the drawn card or error - */ - drawCard(game: GameAggregate, fieldType: 'positive' | 'negative' | 'luck', playerId: string): CardDrawResult { - try { - // Determine which deck type to use based on field type - const deckType = this.getRequiredDeckType(fieldType); - - // Find the appropriate deck in the game - const gameDecks: GameDeck[] = typeof game.gamedecks === 'string' - ? JSON.parse(game.gamedecks) - : game.gamedecks; - - const targetDeck = gameDecks.find((deck: GameDeck) => deck.decktype === deckType); - - if (!targetDeck) { - return { - success: false, - error: `No ${this.getDeckTypeName(deckType)} deck found in game` - }; - } - - // Filter available cards (not played by this player yet) - const availableCards = targetDeck.cards.filter((card: GameCard) => !card.played || card.playerid !== playerId); - - if (availableCards.length === 0) { - return { - success: false, - error: `No more cards available in ${this.getDeckTypeName(deckType)} deck` - }; - } - - // Randomly select a card - const randomIndex = Math.floor(Math.random() * availableCards.length); - const drawnCard = availableCards[randomIndex]; - - // Mark card as drawn by this player - drawnCard.played = true; - drawnCard.playerid = playerId; - - // Check if card has consequence field (joker/luck card) even without type - const hasConsequence = drawnCard.consequence !== undefined && drawnCard.consequence !== null; - - // Prepare client data based on card type - // Only prepare for question cards (cards without consequence and with defined type) - let clientData: CardClientData | undefined; - if (!hasConsequence && drawnCard.type !== undefined) { - try { - clientData = this.cardProcessingService.prepareCardForClient(drawnCard); - } catch (error) { - // If client data preparation fails, still return the card but log the error - console.warn(`Failed to prepare client data for card ${drawnCard.cardid}:`, error); - } - } else if (!hasConsequence && drawnCard.type === undefined) { - // Card is missing type field - this shouldn't happen, log error - console.error(`Card ${drawnCard.cardid} is missing type field. Card data:`, { - cardId: drawnCard.cardid, - hasQuestion: !!drawnCard.question, - hasAnswer: !!drawnCard.answer, - hasConsequence, - cardKeys: Object.keys(drawnCard) - }); - } - - return { - success: true, - card: drawnCard, - clientData: clientData - }; - } catch (error) { - return { - success: false, - error: `Failed to draw card: ${error instanceof Error ? error.message : 'Unknown error'}` - }; - } - } - - /** - * Draw a joker card for secondary landings on special fields - * @param game Game aggregate containing the deck information - * @param playerId ID of the player who needs to draw the joker card - * @returns Card draw result with the joker card or error - */ - drawJokerCard(game: GameAggregate, playerId: string): CardDrawResult { - try { - const gameDecks: GameDeck[] = typeof game.gamedecks === 'string' - ? JSON.parse(game.gamedecks) - : game.gamedecks; - - const jokerDeck = gameDecks.find((deck: GameDeck) => deck.decktype === DeckType.JOCKER); - - if (!jokerDeck) { - return { - success: false, - error: 'No joker deck found in game' - }; - } - - // Filter available joker cards - const availableCards = jokerDeck.cards.filter((card: GameCard) => !card.played || card.playerid !== playerId); - - if (availableCards.length === 0) { - return { - success: false, - error: 'No more joker cards available' - }; - } - - // Randomly select a joker card - const randomIndex = Math.floor(Math.random() * availableCards.length); - const drawnCard = availableCards[randomIndex]; - - // Mark card as drawn by this player - drawnCard.played = true; - drawnCard.playerid = playerId; - - return { - success: true, - card: drawnCard - }; - } catch (error) { - return { - success: false, - error: `Failed to draw joker card: ${error instanceof Error ? error.message : 'Unknown error'}` - }; - } - } - - /** - * Start the answer timeout for a question card - * @param gameId Game ID - * @param playerId Player ID who needs to answer - * @param card The card with the question - * @param onTimeout Callback function when timeout occurs - * @returns Unique key for tracking this pending answer - */ - startAnswerTimeout( - gameId: string, - playerId: string, - card: GameCard, - onTimeout: (gameId: string, playerId: string, card: GameCard) => void - ): string { - const key = `${gameId}:${playerId}`; - - // Clear any existing timeout for this player - this.clearAnswerTimeout(key); - - // Set new timeout - const timeoutId = setTimeout(() => { - onTimeout(gameId, playerId, card); - this.pendingAnswers.delete(key); - }, this.ANSWER_TIMEOUT_MS); - - // Store pending answer - this.pendingAnswers.set(key, { - gameId, - playerId, - card, - timeoutId, - startTime: new Date() - }); - - return key; - } - - /** - * Clear an answer timeout - * @param key The key returned from startAnswerTimeout - */ - clearAnswerTimeout(key: string): void { - const pending = this.pendingAnswers.get(key); - if (pending) { - clearTimeout(pending.timeoutId); - this.pendingAnswers.delete(key); - } - } - - /** - * Process player's answer to a question card - * @param card The question card - * @param playerAnswer Player's submitted answer - * @returns Result indicating if answer was correct and consequence to apply - */ - processAnswer(card: GameCard, playerAnswer: any): CardAnswerResult { - if (!card.answer) { - throw new Error('Card has no answer to compare against'); - } - - let validationResult: CardValidationResult; - - try { - // Use CardProcessingService for type-specific validation - validationResult = this.cardProcessingService.validateAnswer(card, playerAnswer); - } catch (error) { - // Fallback to simple string comparison if type-specific validation fails - console.warn(`Card validation failed, using fallback: ${error}`); - validationResult = this.fallbackValidation(card, playerAnswer); - } - - // For question cards, the consequence is applied only if the answer is correct - // If wrong, we apply a default negative consequence - const consequence = validationResult.isCorrect - ? (card.consequence?.type || ConsequenceType.EXTRA_TURN) - : ConsequenceType.LOSE_TURN; // Default penalty for wrong answer - - return { - correct: validationResult.isCorrect, - consequence: consequence, - description: validationResult.explanation || (validationResult.isCorrect - ? '✅ Correct!' - : '❌ Wrong answer!'), - validationDetails: validationResult - }; - } - - /** - * Process automatic wrong answer (timeout occurred) - * @param card The question card that timed out - * @returns Result with wrong consequence applied - */ - processTimeoutAnswer(card: GameCard): CardAnswerResult { - if (!card.answer) { - throw new Error('Card has no answer to compare against'); - } - - const consequence = ConsequenceType.LOSE_TURN; // Default penalty for timeout - - return { - correct: false, - consequence: consequence, - description: `⏰ Time's up! The correct answer was "${card.answer}". ${this.getConsequenceDescription(consequence, false)}` - }; - } - - /** - * Process luck card effect (no answer required) - * @param card The luck card - * @returns Result with the luck consequence to apply - */ - processLuckCard(card: GameCard): CardAnswerResult { - const consequence = card.consequence?.type || ConsequenceType.EXTRA_TURN; - - return { - correct: true, // Luck cards are always "correct" since no answer is needed - consequence: consequence, - description: `🍀 ${this.getConsequenceDescription(consequence, true)}` - }; - } - - /** - * Get the required deck type based on field type - */ - private getRequiredDeckType(fieldType: 'positive' | 'negative' | 'luck'): DeckType { - switch (fieldType) { - case 'positive': - case 'negative': - return DeckType.QUEST; // Question cards for positive/negative fields - case 'luck': - return DeckType.LUCK; // Luck cards for luck fields - default: - throw new Error(`Unsupported field type: ${fieldType}`); - } - } - - /** - * Get human-readable deck type name - */ - private getDeckTypeName(deckType: DeckType): string { - switch (deckType) { - case DeckType.QUEST: - return 'question'; - case DeckType.LUCK: - return 'luck'; - case DeckType.JOCKER: - return 'joker'; - default: - return 'unknown'; - } - } - - /** - * Get human-readable consequence description - */ - private getConsequenceDescription(consequence: ConsequenceType, isPositive: boolean): string { - switch (consequence) { - case ConsequenceType.MOVE_FORWARD: - return isPositive ? 'Move forward!' : 'Move forward anyway!'; - case ConsequenceType.MOVE_BACKWARD: - return 'Move backward!'; - case ConsequenceType.LOSE_TURN: - return 'Lose your next turn!'; - case ConsequenceType.EXTRA_TURN: - return 'Get an extra turn!'; - case ConsequenceType.GO_TO_START: - return 'Go back to start!'; - default: - return 'Unknown effect!'; - } - } - - /** - * Get remaining time for a pending answer - * @param key The key for the pending answer - * @returns Remaining time in seconds, or -1 if not found - */ - getRemainingTime(key: string): number { - const pending = this.pendingAnswers.get(key); - if (!pending) { - return -1; - } - - const elapsed = Date.now() - pending.startTime.getTime(); - const remaining = Math.max(0, this.ANSWER_TIMEOUT_MS - elapsed); - return Math.ceil(remaining / 1000); // Return in seconds - } - - /** - * Check if a player has a pending answer - * @param gameId Game ID - * @param playerId Player ID - * @returns True if player has a pending answer - */ - hasPendingAnswer(gameId: string, playerId: string): boolean { - const key = `${gameId}:${playerId}`; - return this.pendingAnswers.has(key); - } - - /** - * Fallback validation for cards without proper type information - * @param card The card to validate - * @param playerAnswer Player's answer - * @returns Basic validation result - */ - private fallbackValidation(card: GameCard, playerAnswer: any): CardValidationResult { - if (typeof card.answer !== 'string' || typeof playerAnswer !== 'string') { - return { - isCorrect: false, - submittedAnswer: playerAnswer, - explanation: 'Cannot validate non-string answers without card type information' - }; - } - - const cleanPlayerAnswer = playerAnswer.toLowerCase().trim(); - const cleanCorrectAnswer = card.answer.toLowerCase().trim(); - const isCorrect = cleanPlayerAnswer === cleanCorrectAnswer; - - return { - isCorrect, - submittedAnswer: playerAnswer, - correctAnswer: card.answer, - explanation: isCorrect - ? '✅ Correct!' - : `❌ Wrong! The correct answer was "${card.answer}".` - }; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/CardProcessingService.ts b/SerpentRace_Backend/src/Application/Services/CardProcessingService.ts deleted file mode 100644 index 65a0fa27..00000000 --- a/SerpentRace_Backend/src/Application/Services/CardProcessingService.ts +++ /dev/null @@ -1,430 +0,0 @@ -import { GameCard } from '../../Domain/Game/GameAggregate'; -import { CardType } from '../../Domain/Deck/DeckAggregate'; - -// Type-specific answer structures -export interface QuizOption { - answer: string; // A, B, C, D - text: string; - correct: boolean; -} - -export interface CloserAnswer { - correct: number; - percent: number; -} - -/** - * Sentence pair for matching left to right - */ -export interface SentencePair { - id: string; // Unique identifier for this pair - left: string; // Left part to match - right: string; // Right part (scrambled position) -} - -/** - * Player's answer for sentence pairing (array of matches) - */ -export interface SentencePairingAnswer { - pairId: string; // ID of the pair - leftText: string; // Left part - rightText: string; // Player's chosen right part -} - -export interface CardClientData { - cardid: string; - question: string; - type: CardType; - timeLimit: number; - // Type-specific client data - answerOptions?: QuizOption[]; // For QUIZ - words?: string[]; // For SENTENCE_PAIRING (legacy scrambled words) - sentencePairs?: SentencePair[]; // For SENTENCE_PAIRING (left-right matching) - acceptableAnswers?: string[]; // For OWN_ANSWER (not sent to client) - // CLOSER and TRUE_FALSE send only question -} - -export interface CardValidationResult { - isCorrect: boolean; - submittedAnswer: any; - correctAnswer?: any; - explanation?: string; -} - -/** - * Service responsible for handling type-specific card processing - * Prepares cards for clients and validates answers based on CardType - */ -export class CardProcessingService { - - /** - * Prepare card data for client based on card type - * @param card The game card to prepare - * @returns Client-safe card data with type-specific information - */ - prepareCardForClient(card: GameCard): CardClientData { - if (!card.question || card.type === undefined) { - throw new Error('Card must have question and type defined'); - } - - const baseData: CardClientData = { - cardid: card.cardid, - question: card.question, - type: card.type, - timeLimit: 60 // Default 60 seconds for question cards - }; - - switch (card.type) { - case CardType.QUIZ: - return this.prepareQuizCard(card, baseData); - - case CardType.SENTENCE_PAIRING: - return this.prepareSentencePairingCard(card, baseData); - - case CardType.OWN_ANSWER: - return this.prepareOwnAnswerCard(card, baseData); - - case CardType.TRUE_FALSE: - return this.prepareTrueFalseCard(card, baseData); - - case CardType.CLOSER: - return this.prepareCloserCard(card, baseData); - - default: - throw new Error(`Unsupported card type: ${card.type}`); - } - } - - /** - * Validate player's answer based on card type - * @param card The game card - * @param playerAnswer Player's submitted answer - * @returns Validation result with correctness and explanation - */ - validateAnswer(card: GameCard, playerAnswer: any): CardValidationResult { - if (card.type === undefined) { - throw new Error('Card type is required for validation'); - } - - switch (card.type) { - case CardType.QUIZ: - return this.validateQuizAnswer(card, playerAnswer); - - case CardType.SENTENCE_PAIRING: - return this.validateSentencePairingAnswer(card, playerAnswer); - - case CardType.OWN_ANSWER: - return this.validateOwnAnswerAnswer(card, playerAnswer); - - case CardType.TRUE_FALSE: - return this.validateTrueFalseAnswer(card, playerAnswer); - - case CardType.CLOSER: - return this.validateCloserAnswer(card, playerAnswer); - - default: - throw new Error(`Unsupported card type for validation: ${card.type}`); - } - } - - /** - * Prepare QUIZ card with multiple choice options - */ - private prepareQuizCard(card: GameCard, baseData: CardClientData): CardClientData { - if (!Array.isArray(card.answer)) { - throw new Error('Quiz card answer must be an array of options'); - } - - return { - ...baseData, - answerOptions: card.answer as QuizOption[] - }; - } - - /** - * Prepare SENTENCE_PAIRING card with scrambled left/right pairs - * - * Expected card.answer format: - * [ - * { left: "Apple", right: "Red" }, - * { left: "Banana", right: "Yellow" }, - * { left: "Orange", right: "Orange color" } - * ] - * - * OR legacy string format: "word1 word2 word3" (will be split and scrambled) - */ - private prepareSentencePairingCard(card: GameCard, baseData: CardClientData): CardClientData { - // NEW FORMAT: Array of pairs (left-right matching) - if (Array.isArray(card.answer)) { - // Validate structure - const pairs = card.answer as Array<{ left: string; right: string }>; - if (!pairs.every(p => p.left && p.right)) { - throw new Error('Sentence pairing card answer must be array of {left, right} objects'); - } - - // Create pairs with IDs and scramble the right parts - const leftParts = pairs.map((p, idx) => ({ id: `pair_${idx}`, left: p.left, right: p.right })); - const rightParts = this.scrambleArray([...pairs.map(p => p.right)]); - - // Send left parts in order, right parts scrambled - const sentencePairs: SentencePair[] = leftParts.map((lp, idx) => ({ - id: lp.id, - left: lp.left, - right: rightParts[idx] // Scrambled position - })); - - return { - ...baseData, - sentencePairs - }; - } - - // LEGACY FORMAT: Single sentence to reconstruct (backward compatibility) - if (typeof card.answer === 'string') { - const words = card.answer.split(' ').filter(word => word.trim() !== ''); - const scrambledWords = this.scrambleArray([...words]); - - return { - ...baseData, - words: scrambledWords - }; - } - - throw new Error('Sentence pairing card answer must be array of pairs or string'); - } - - /** - * Prepare OWN_ANSWER card (only question, acceptable answers hidden) - */ - private prepareOwnAnswerCard(card: GameCard, baseData: CardClientData): CardClientData { - // Don't send acceptable answers to client - return baseData; - } - - /** - * Prepare TRUE_FALSE card (only question) - */ - private prepareTrueFalseCard(card: GameCard, baseData: CardClientData): CardClientData { - return baseData; - } - - /** - * Prepare CLOSER card (only question) - */ - private prepareCloserCard(card: GameCard, baseData: CardClientData): CardClientData { - return baseData; - } - - /** - * Validate QUIZ answer (A, B, C, D) - */ - private validateQuizAnswer(card: GameCard, playerAnswer: string): CardValidationResult { - if (!Array.isArray(card.answer)) { - throw new Error('Quiz card answer must be an array'); - } - - const options = card.answer as QuizOption[]; - const correctOption = options.find(opt => opt.correct); - - if (!correctOption) { - throw new Error('Quiz card must have one correct answer'); - } - - const isCorrect = playerAnswer.toUpperCase() === correctOption.answer.toUpperCase(); - - return { - isCorrect, - submittedAnswer: playerAnswer, - correctAnswer: correctOption.answer, - explanation: isCorrect - ? `✅ Correct! ${correctOption.text}` - : `❌ Wrong! Correct answer was ${correctOption.answer}: ${correctOption.text}` - }; - } - - /** - * Validate SENTENCE_PAIRING answer - * - * Supports two formats: - * 1. NEW: Array of { pairId, leftText, rightText } matches - * 2. LEGACY: Reconstructed sentence string or array of words - */ - private validateSentencePairingAnswer(card: GameCard, playerAnswer: any): CardValidationResult { - // NEW FORMAT: Array of pairs (left-right matching) - if (Array.isArray(card.answer) && card.answer.every((p: any) => p.left && p.right)) { - const correctPairs = card.answer as Array<{ left: string; right: string }>; - - // Player answer should be array of SentencePairingAnswer objects - if (!Array.isArray(playerAnswer)) { - throw new Error('Player answer must be array of pair matches'); - } - - const playerMatches = playerAnswer as SentencePairingAnswer[]; - - // Check if all pairs match correctly - let correctCount = 0; - const results: string[] = []; - - for (const correctPair of correctPairs) { - const playerMatch = playerMatches.find(pm => - pm.leftText.toLowerCase().trim() === correctPair.left.toLowerCase().trim() - ); - - if (playerMatch) { - const isMatch = playerMatch.rightText.toLowerCase().trim() === - correctPair.right.toLowerCase().trim(); - if (isMatch) { - correctCount++; - results.push(`✓ "${correctPair.left}" → "${correctPair.right}"`); - } else { - results.push(`✗ "${correctPair.left}" → "${playerMatch.rightText}" (should be "${correctPair.right}")`); - } - } else { - results.push(`✗ "${correctPair.left}" → (not matched)`); - } - } - - const isCorrect = correctCount === correctPairs.length; - - return { - isCorrect, - submittedAnswer: playerMatches, - correctAnswer: correctPairs, - explanation: isCorrect - ? `✅ Perfect! All ${correctCount} pairs matched correctly!\n${results.join('\n')}` - : `❌ Only ${correctCount}/${correctPairs.length} pairs correct:\n${results.join('\n')}` - }; - } - - // LEGACY FORMAT: Single sentence to reconstruct (backward compatibility) - if (typeof card.answer === 'string') { - // Handle both array of words and joined string - const reconstructed = Array.isArray(playerAnswer) - ? playerAnswer.join(' ').toLowerCase().trim() - : (typeof playerAnswer === 'string' ? playerAnswer.toLowerCase().trim() : ''); - - const correctSentence = card.answer.toLowerCase().trim(); - const isCorrect = reconstructed === correctSentence; - - return { - isCorrect, - submittedAnswer: reconstructed, - correctAnswer: card.answer, - explanation: isCorrect - ? '✅ Perfect! You arranged the sentence correctly!' - : `❌ Wrong order! Correct sentence: "${card.answer}"` - }; - } - - throw new Error('Sentence pairing card answer must be array of pairs or string'); - } - - /** - * Validate OWN_ANSWER (check against acceptable answers array) - */ - private validateOwnAnswerAnswer(card: GameCard, playerAnswer: string): CardValidationResult { - if (!Array.isArray(card.answer)) { - throw new Error('Own answer card must have array of acceptable answers'); - } - - const acceptableAnswers = card.answer as string[]; - const cleanPlayerAnswer = playerAnswer.toLowerCase().trim(); - - const isCorrect = acceptableAnswers.some(acceptable => - acceptable.toLowerCase().trim() === cleanPlayerAnswer - ); - - return { - isCorrect, - submittedAnswer: playerAnswer, - correctAnswer: acceptableAnswers, - explanation: isCorrect - ? '✅ Correct! Your answer is acceptable.' - : `❌ Your answer doesn't match any acceptable responses.` - }; - } - - /** - * Validate TRUE_FALSE answer - */ - private validateTrueFalseAnswer(card: GameCard, playerAnswer: string): CardValidationResult { - if (typeof card.answer !== 'boolean' && typeof card.answer !== 'string') { - throw new Error('True/false card answer must be boolean or string'); - } - - // Convert player answer to boolean - const playerBool = this.convertToBoolean(playerAnswer); - const correctBool = typeof card.answer === 'boolean' - ? card.answer - : this.convertToBoolean(card.answer); - - const isCorrect = playerBool === correctBool; - - return { - isCorrect, - submittedAnswer: playerAnswer, - correctAnswer: correctBool ? 'True' : 'False', - explanation: isCorrect - ? '✅ Correct!' - : `❌ Wrong! The correct answer is ${correctBool ? 'True' : 'False'}.` - }; - } - - /** - * Validate CLOSER answer (numerical proximity) - */ - private validateCloserAnswer(card: GameCard, playerAnswer: string | number): CardValidationResult { - if (typeof card.answer !== 'object' || !card.answer.correct || !card.answer.percent) { - throw new Error('Closer card answer must have correct and percent fields'); - } - - const closerAnswer = card.answer as CloserAnswer; - const playerNumber = typeof playerAnswer === 'number' - ? playerAnswer - : parseFloat(playerAnswer.toString()); - - if (isNaN(playerNumber)) { - return { - isCorrect: false, - submittedAnswer: playerAnswer, - correctAnswer: closerAnswer.correct, - explanation: '❌ Invalid number! Please enter a valid numeric answer.' - }; - } - - const tolerance = closerAnswer.correct * (closerAnswer.percent / 100); - const minValue = closerAnswer.correct - tolerance; - const maxValue = closerAnswer.correct + tolerance; - - const isCorrect = playerNumber >= minValue && playerNumber <= maxValue; - - return { - isCorrect, - submittedAnswer: playerNumber, - correctAnswer: closerAnswer.correct, - explanation: isCorrect - ? `✅ Close enough! Correct answer: ${closerAnswer.correct}` - : `❌ Not close enough! Correct answer: ${closerAnswer.correct} (±${closerAnswer.percent}%)` - }; - } - - /** - * Convert string to boolean for TRUE_FALSE validation - */ - private convertToBoolean(value: string): boolean { - const lowerValue = value.toLowerCase().trim(); - return ['true', 'yes', '1', 'correct', 'right', 'igaz'].includes(lowerValue); - } - - /** - * Scramble array elements randomly - */ - private scrambleArray(array: T[]): T[] { - const scrambled = [...array]; - for (let i = scrambled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [scrambled[i], scrambled[j]] = [scrambled[j], scrambled[i]]; - } - return scrambled; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/ContactEmailService.ts b/SerpentRace_Backend/src/Application/Services/ContactEmailService.ts deleted file mode 100644 index 969537e4..00000000 --- a/SerpentRace_Backend/src/Application/Services/ContactEmailService.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { IContactRepository } from '../../Domain/IRepository/IContactRepository'; -import { EmailService } from './EmailService'; -import { ContactType } from '../../Domain/Contact/ContactAggregate'; -import { logOther, logError } from './Logger'; -import { EmailTemplateHelper, LocalizedSubjects } from './EmailTemplateHelper'; - -export interface EmailResponseData { - to: string; - message: string; - contactId: string; - adminUserId: string; - contactName: string; - contactType: ContactType; - originalMessage: string; - language?: 'en' | 'hu' | 'de'; // Default to 'en' if not specified -} - -export class ContactEmailService { - constructor( - private readonly contactRepo: IContactRepository, - private readonly emailService: EmailService - ) {} - - async sendResponse(responseData: EmailResponseData): Promise { - try { - // First update the contact with the response - await this.contactRepo.update(responseData.contactId, { - adminResponse: responseData.message, - responseDate: new Date(), - respondedBy: responseData.adminUserId, - }); - - // Determine language and template - const language = responseData.language || 'en'; - const templateName = language === 'en' ? 'contact-response' : `contact-response-${language}`; - - // Prepare template data - const templateData = { - contactName: responseData.contactName, - contactTypeString: this.getContactTypeString(responseData.contactType, language), - contactTypeBadge: this.getContactTypeBadge(responseData.contactType), - originalMessage: responseData.originalMessage, - adminResponse: responseData.message, - companyName: 'SerpentRace', - supportEmail: 'support@serpentrace.com' - }; - - // Send email using EmailService with template - const emailSent = await this.emailService.sendEmail({ - to: responseData.to, - subject: this.getLocalizedContactResponseSubject(language), - template: templateName, - templateData - }); - - if (emailSent) { - logOther('Contact response email sent successfully', { - to: responseData.to, - subject: this.getLocalizedContactResponseSubject(language), - contactId: responseData.contactId, - respondedBy: responseData.adminUserId, - language - }); - } else { - throw new Error('Email service failed to send email'); - } - - } catch (error) { - logError('Failed to send contact response email', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to send email response'); - } - } - - private getLocalizedContactResponseSubject(language: 'en' | 'hu' | 'de'): string { - const subjects: LocalizedSubjects = { - contactResponse: { - en: 'SerpentRace - Response to Your Message', - hu: 'SerpentRace - Válasz az üzenetére', - de: 'SerpentRace - Antwort auf Ihre Nachricht' - } - }; - return EmailTemplateHelper.getLocalizedSubject('contactResponse', subjects, language); - } - - private getContactTypeString(type: ContactType, language: 'en' | 'hu' | 'de' = 'en'): string { - const translations = { - [ContactType.BUG]: { - en: 'Bug Report', - hu: 'Hiba bejelentés', - de: 'Fehlerbericht' - }, - [ContactType.PROBLEM]: { - en: 'Problem', - hu: 'Probléma', - de: 'Problem' - }, - [ContactType.QUESTION]: { - en: 'Question', - hu: 'Kérdés', - de: 'Frage' - }, - [ContactType.SALES]: { - en: 'Sales Inquiry', - hu: 'Értékesítési kérdés', - de: 'Verkaufsanfrage' - }, - [ContactType.OTHER]: { - en: 'General Inquiry', - hu: 'Általános kérdés', - de: 'Allgemeine Anfrage' - } - }; - - return translations[type]?.[language] || translations[type]?.['en'] || 'Contact'; - } - - private getContactTypeBadge(type: ContactType): string { - switch (type) { - case ContactType.BUG: - return 'bug'; - case ContactType.PROBLEM: - return 'problem'; - case ContactType.QUESTION: - return 'question'; - case ContactType.SALES: - return 'sales'; - case ContactType.OTHER: - return 'other'; - default: - return 'other'; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Services/DIContainer.ts b/SerpentRace_Backend/src/Application/Services/DIContainer.ts deleted file mode 100644 index 0962d215..00000000 --- a/SerpentRace_Backend/src/Application/Services/DIContainer.ts +++ /dev/null @@ -1,600 +0,0 @@ -// Repository Interfaces -import { IUserRepository } from '../../Domain/IRepository/IUserRepository'; -import { IChatRepository } from '../../Domain/IRepository/IChatRepository'; -import { IChatArchiveRepository } from '../../Domain/IRepository/IChatArchiveRepository'; -import { IDeckRepository } from '../../Domain/IRepository/IDeckRepository'; -import { IOrganizationRepository } from '../../Domain/IRepository/IOrganizationRepository'; -import { IContactRepository } from '../../Domain/IRepository/IContactRepository'; -import { IGameRepository } from '../../Domain/IRepository/IGameRepository'; -import { ITurnHistoryRepository } from '../../Domain/IRepository/ITurnHistoryRepository'; -import { IGameSnapshotRepository } from '../../Domain/IRepository/IGameSnapshotRepository'; - -// Repository Implementations -import { UserRepository } from '../../Infrastructure/Repository/UserRepository'; -import { ChatRepository } from '../../Infrastructure/Repository/ChatRepository'; -import { ChatArchiveRepository } from '../../Infrastructure/Repository/ChatArchiveRepository'; -import { DeckRepository } from '../../Infrastructure/Repository/DeckRepository'; -import { OrganizationRepository } from '../../Infrastructure/Repository/OrganizationRepository'; -import { ContactRepository } from '../../Infrastructure/Repository/ContactRepository'; -import { GameRepository } from '../../Infrastructure/Repository/GameRepository'; -import { TurnHistoryRepository } from '../../Infrastructure/Repository/TurnHistoryRepository'; -import { GameSnapshotRepository } from '../../Infrastructure/Repository/GameSnapshotRepository'; - -// Command Handlers -import { CreateUserCommandHandler } from '../User/commands/CreateUserCommandHandler'; -import { LoginCommandHandler } from '../User/commands/LoginCommandHandler'; -import { LogoutCommandHandler } from '../User/commands/LogoutCommandHandler'; -import { UpdateUserCommandHandler } from '../User/commands/UpdateUserCommandHandler'; -import { DeactivateUserCommandHandler } from '../User/commands/DeactivateUserCommandHandler'; -import { DeleteUserCommandHandler } from '../User/commands/DeleteUserCommandHandler'; -import { VerifyEmailCommandHandler } from '../User/commands/VerifyEmailCommandHandler'; -import { RequestPasswordResetCommandHandler } from '../User/commands/RequestPasswordResetCommandHandler'; -import { ResetPasswordCommandHandler } from '../User/commands/ResetPasswordCommandHandler'; -import { CreateChatCommandHandler } from '../Chat/commands/CreateChatCommandHandler'; -import { SendMessageCommandHandler } from '../Chat/commands/SendMessageCommandHandler'; -import { ArchiveChatCommandHandler, RestoreChatCommandHandler } from '../Chat/commands/ChatArchiveCommandHandlers'; -import { CreateDeckCommandHandler } from '../Deck/commands/CreateDeckCommandHandler'; -import { UpdateDeckCommandHandler } from '../Deck/commands/UpdateDeckCommandHandler'; -import { DeleteDeckCommandHandler } from '../Deck/commands/DeleteDeckCommandHandler'; -import { CreateOrganizationCommandHandler } from '../Organization/commands/CreateOrganizationCommandHandler'; -import { UpdateOrganizationCommandHandler } from '../Organization/commands/UpdateOrganizationCommandHandler'; -import { DeleteOrganizationCommandHandler } from '../Organization/commands/DeleteOrganizationCommandHandler'; -import { ProcessOrgAuthCallbackCommandHandler } from '../Organization/commands/ProcessOrgAuthCallbackCommandHandler'; -import { CreateContactCommandHandler } from '../Contact/commands/CreateContactCommandHandler'; -import { UpdateContactCommandHandler } from '../Contact/commands/UpdateContactCommandHandler'; -import { DeleteContactCommandHandler } from '../Contact/commands/DeleteContactCommandHandler'; -import { ActivateUserCommandHandler } from '../User/commands/ActivateUserCommandHandler'; - -// Query Handlers -import { GetUserByIdQueryHandler } from '../User/queries/GetUserByIdQueryHandler'; -import { GetUsersByPageQueryHandler } from '../User/queries/GetUsersByPageQueryHandler'; -import { GetUserChatsQueryHandler } from '../Chat/queries/GetUserChatsQueryHandler'; -import { GetChatHistoryQueryHandler, GetArchivedChatsQueryHandler } from '../Chat/queries/ChatHistoryQueryHandlers'; -import { GetChatsByPageQueryHandler } from '../Chat/queries/GetChatsByPageQueryHandler'; -import { GetDeckByIdQueryHandler } from '../Deck/queries/GetDeckByIdQueryHandler'; -import { GetDecksByPageQueryHandler } from '../Deck/queries/GetDecksByPageQueryHandler'; -import { GetOrganizationByIdQueryHandler } from '../Organization/queries/GetOrganizationByIdQueryHandler'; -import { GetOrganizationsByPageQueryHandler } from '../Organization/queries/GetOrganizationsByPageQueryHandler'; -import { GetOrganizationLoginUrlQueryHandler } from '../Organization/queries/GetOrganizationLoginUrlQueryHandler'; -import { GetContactByIdQueryHandler } from '../Contact/queries/GetContactByIdQueryHandler'; -import { GetContactsByPageQueryHandler } from '../Contact/queries/GetContactsByPageQueryHandler'; - -// Services -import { JWTService } from './JWTService'; -import { EmailService } from './EmailService'; -import { GameTokenService } from './GameTokenService'; -import { ContactEmailService } from './ContactEmailService'; -import { DeckImportExportService } from './DeckImportExportService'; -import { FieldEffectService } from './FieldEffectService'; -import { CardDrawingService } from './CardDrawingService'; -import { GamemasterService } from './GamemasterService'; -import { RedisService } from './RedisService'; -import { GameService } from '../Game/GameService'; -import { BoardGenerationService } from '../Game/BoardGenerationService'; -import { GenerateBoardCommandHandler } from '../Game/commands/GenerateBoardCommandHandler'; -import { GameWebSocketService } from './GameWebSocketService'; -import type { Server as SocketIOServer } from 'socket.io'; - -/** - * Central Dependency Injection Container - * Manages all repositories, command handlers, and query handlers as singletons - */ -export class DIContainer { - private static instance: DIContainer; - - // Repositories - Using interfaces for better abstraction - private _userRepository: IUserRepository | null = null; - private _chatRepository: IChatRepository | null = null; - private _chatArchiveRepository: IChatArchiveRepository | null = null; - private _deckRepository: IDeckRepository | null = null; - private _organizationRepository: IOrganizationRepository | null = null; - private _contactRepository: IContactRepository | null = null; - private _gameRepository: IGameRepository | null = null; - private _turnHistoryRepository: ITurnHistoryRepository | null = null; - private _gameSnapshotRepository: IGameSnapshotRepository | null = null; - - // Services - private _jwtService: JWTService | null = null; - private _emailService: EmailService | null = null; - private _gameTokenService: GameTokenService | null = null; - private _contactEmailService: ContactEmailService | null = null; - private _deckImportExportService: DeckImportExportService | null = null; - private _cardDrawingService: CardDrawingService | null = null; - private _gamemasterService: GamemasterService | null = null; - private _fieldEffectService: FieldEffectService | null = null; - private _gameService: GameService | null = null; - private _boardGenerationService: BoardGenerationService | null = null; - private _gameWebSocketService: GameWebSocketService | null = null; - private _socketIOInstance: SocketIOServer | null = null; - - // Command Handlers - private _createUserCommandHandler: CreateUserCommandHandler | null = null; - private _loginCommandHandler: LoginCommandHandler | null = null; - private _logoutCommandHandler: LogoutCommandHandler | null = null; - private _updateUserCommandHandler: UpdateUserCommandHandler | null = null; - private _deactivateUserCommandHandler: DeactivateUserCommandHandler | null = null; - private _deleteUserCommandHandler: DeleteUserCommandHandler | null = null; - private _verifyEmailCommandHandler: VerifyEmailCommandHandler | null = null; - private _requestPasswordResetCommandHandler: RequestPasswordResetCommandHandler | null = null; - private _resetPasswordCommandHandler: ResetPasswordCommandHandler | null = null; - private _createChatCommandHandler: CreateChatCommandHandler | null = null; - private _sendMessageCommandHandler: SendMessageCommandHandler | null = null; - private _archiveChatCommandHandler: ArchiveChatCommandHandler | null = null; - private _restoreChatCommandHandler: RestoreChatCommandHandler | null = null; - private _createDeckCommandHandler: CreateDeckCommandHandler | null = null; - private _updateDeckCommandHandler: UpdateDeckCommandHandler | null = null; - private _deleteDeckCommandHandler: DeleteDeckCommandHandler | null = null; - private _createOrganizationCommandHandler: CreateOrganizationCommandHandler | null = null; - private _updateOrganizationCommandHandler: UpdateOrganizationCommandHandler | null = null; - private _deleteOrganizationCommandHandler: DeleteOrganizationCommandHandler | null = null; - private _processOrgAuthCallbackCommandHandler: ProcessOrgAuthCallbackCommandHandler | null = null; - private _createContactCommandHandler: CreateContactCommandHandler | null = null; - private _updateContactCommandHandler: UpdateContactCommandHandler | null = null; - private _deleteContactCommandHandler: DeleteContactCommandHandler | null = null; - private _generateBoardCommandHandler: GenerateBoardCommandHandler | null = null; - private _activateUserCommandHandler: ActivateUserCommandHandler | null = null; - - // Query Handlers - private _getUserByIdQueryHandler: GetUserByIdQueryHandler | null = null; - private _getUsersByPageQueryHandler: GetUsersByPageQueryHandler | null = null; - private _getUserChatsQueryHandler: GetUserChatsQueryHandler | null = null; - private _getChatHistoryQueryHandler: GetChatHistoryQueryHandler | null = null; - private _getArchivedChatsQueryHandler: GetArchivedChatsQueryHandler | null = null; - private _getChatsByPageQueryHandler: GetChatsByPageQueryHandler | null = null; - private _getDeckByIdQueryHandler: GetDeckByIdQueryHandler | null = null; - private _getDecksByPageQueryHandler: GetDecksByPageQueryHandler | null = null; - private _getOrganizationByIdQueryHandler: GetOrganizationByIdQueryHandler | null = null; - private _getOrganizationsByPageQueryHandler: GetOrganizationsByPageQueryHandler | null = null; - private _getOrganizationLoginUrlQueryHandler: GetOrganizationLoginUrlQueryHandler | null = null; - private _getContactByIdQueryHandler: GetContactByIdQueryHandler | null = null; - private _getContactsByPageQueryHandler: GetContactsByPageQueryHandler | null = null; - - private constructor() {} - - public static getInstance(): DIContainer { - if (!DIContainer.instance) { - DIContainer.instance = new DIContainer(); - } - return DIContainer.instance; - } - - // Repository getters - Return interfaces for better abstraction - public get userRepository(): IUserRepository { - if (!this._userRepository) { - this._userRepository = new UserRepository(); - } - return this._userRepository; - } - - public get chatRepository(): IChatRepository { - if (!this._chatRepository) { - this._chatRepository = new ChatRepository(); - } - return this._chatRepository; - } - - public get chatArchiveRepository(): IChatArchiveRepository { - if (!this._chatArchiveRepository) { - this._chatArchiveRepository = new ChatArchiveRepository(); - } - return this._chatArchiveRepository; - } - - public get deckRepository(): IDeckRepository { - if (!this._deckRepository) { - this._deckRepository = new DeckRepository(); - } - return this._deckRepository; - } - - public get organizationRepository(): IOrganizationRepository { - if (!this._organizationRepository) { - this._organizationRepository = new OrganizationRepository(); - } - return this._organizationRepository; - } - - public get contactRepository(): IContactRepository { - if (!this._contactRepository) { - this._contactRepository = new ContactRepository(); - } - return this._contactRepository; - } - - public get gameRepository(): IGameRepository { - if (!this._gameRepository) { - this._gameRepository = new GameRepository(); - } - return this._gameRepository; - } - - public get turnHistoryRepository(): ITurnHistoryRepository { - if (!this._turnHistoryRepository) { - this._turnHistoryRepository = new TurnHistoryRepository(); - } - return this._turnHistoryRepository; - } - - public get gameSnapshotRepository(): IGameSnapshotRepository { - if (!this._gameSnapshotRepository) { - this._gameSnapshotRepository = new GameSnapshotRepository(); - } - return this._gameSnapshotRepository; - } - - // Services getters - public get jwtService(): JWTService { - if (!this._jwtService) { - this._jwtService = new JWTService(); - } - return this._jwtService; - } - - public get emailService(): EmailService { - if (!this._emailService) { - this._emailService = new EmailService(); - } - return this._emailService; - } - - public get gameTokenService(): GameTokenService { - if (!this._gameTokenService) { - this._gameTokenService = new GameTokenService(); - } - return this._gameTokenService; - } - - public get contactEmailService(): ContactEmailService { - if (!this._contactEmailService) { - this._contactEmailService = new ContactEmailService(this.contactRepository, this.emailService); - } - return this._contactEmailService; - } - - public get deckImportExportService(): DeckImportExportService { - if (!this._deckImportExportService) { - this._deckImportExportService = new DeckImportExportService(this.deckRepository); - } - return this._deckImportExportService; - } - - public get cardDrawingService(): CardDrawingService { - if (!this._cardDrawingService) { - this._cardDrawingService = new CardDrawingService(); - } - return this._cardDrawingService; - } - - public get gamemasterService(): GamemasterService { - if (!this._gamemasterService) { - this._gamemasterService = new GamemasterService(); - } - return this._gamemasterService; - } - - public get fieldEffectService(): FieldEffectService { - if (!this._fieldEffectService) { - this._fieldEffectService = new FieldEffectService( - this.boardGenerationService, - this.gamemasterService - ); - } - return this._fieldEffectService; - } - - public get gameService(): GameService { - if (!this._gameService) { - this._gameService = new GameService(); - } - return this._gameService; - } - - public get boardGenerationService(): BoardGenerationService { - if (!this._boardGenerationService) { - this._boardGenerationService = new BoardGenerationService(); - } - return this._boardGenerationService; - } - - /** - * Set the Socket.IO instance (must be called before accessing gameWebSocketService) - */ - public setSocketIO(io: SocketIOServer): void { - this._socketIOInstance = io; - // Reset gameWebSocketService so it gets recreated with new IO instance - this._gameWebSocketService = null; - } - - public get gameWebSocketService(): GameWebSocketService { - if (!this._gameWebSocketService) { - if (!this._socketIOInstance) { - throw new Error('Socket.IO instance must be set before accessing gameWebSocketService. Call setSocketIO() first.'); - } - this._gameWebSocketService = new GameWebSocketService( - this._socketIOInstance, - this.gameRepository as any, // Cast to concrete type - this.userRepository as any, // Cast to concrete type - RedisService.getInstance(), - this.turnHistoryRepository as any, // Cast to concrete type - this.gameSnapshotRepository as any // Cast to concrete type - ); - } - return this._gameWebSocketService; - } - - // Command Handler getters - public get createUserCommandHandler(): CreateUserCommandHandler { - if (!this._createUserCommandHandler) { - this._createUserCommandHandler = new CreateUserCommandHandler(this.userRepository, this.emailService); - } - return this._createUserCommandHandler; - } - - public get loginCommandHandler(): LoginCommandHandler { - if (!this._loginCommandHandler) { - this._loginCommandHandler = new LoginCommandHandler(this.userRepository, this.jwtService, this.organizationRepository); - } - return this._loginCommandHandler; - } - - public get logoutCommandHandler(): LogoutCommandHandler { - if (!this._logoutCommandHandler) { - this._logoutCommandHandler = new LogoutCommandHandler(this.userRepository); - } - return this._logoutCommandHandler; - } - - public get updateUserCommandHandler(): UpdateUserCommandHandler { - if (!this._updateUserCommandHandler) { - this._updateUserCommandHandler = new UpdateUserCommandHandler(this.userRepository); - } - return this._updateUserCommandHandler; - } - - public get deactivateUserCommandHandler(): DeactivateUserCommandHandler { - if (!this._deactivateUserCommandHandler) { - this._deactivateUserCommandHandler = new DeactivateUserCommandHandler(this.userRepository); - } - return this._deactivateUserCommandHandler; - } - - public get activateUserCommandHandler(): ActivateUserCommandHandler { - if (!this._activateUserCommandHandler) { - this._activateUserCommandHandler = new ActivateUserCommandHandler(this.userRepository); - } - return this._activateUserCommandHandler; - } - - public get deleteUserCommandHandler(): DeleteUserCommandHandler { - if (!this._deleteUserCommandHandler) { - this._deleteUserCommandHandler = new DeleteUserCommandHandler(this.userRepository); - } - return this._deleteUserCommandHandler; - } - - public get verifyEmailCommandHandler(): VerifyEmailCommandHandler { - if (!this._verifyEmailCommandHandler) { - this._verifyEmailCommandHandler = new VerifyEmailCommandHandler(this.userRepository); - } - return this._verifyEmailCommandHandler; - } - - public get requestPasswordResetCommandHandler(): RequestPasswordResetCommandHandler { - if (!this._requestPasswordResetCommandHandler) { - this._requestPasswordResetCommandHandler = new RequestPasswordResetCommandHandler(this.userRepository, this.emailService); - } - return this._requestPasswordResetCommandHandler; - } - - public get resetPasswordCommandHandler(): ResetPasswordCommandHandler { - if (!this._resetPasswordCommandHandler) { - this._resetPasswordCommandHandler = new ResetPasswordCommandHandler(this.userRepository); - } - return this._resetPasswordCommandHandler; - } - - public get createChatCommandHandler(): CreateChatCommandHandler { - if (!this._createChatCommandHandler) { - this._createChatCommandHandler = new CreateChatCommandHandler(this.chatRepository, this.userRepository); - } - return this._createChatCommandHandler; - } - - public get sendMessageCommandHandler(): SendMessageCommandHandler { - if (!this._sendMessageCommandHandler) { - this._sendMessageCommandHandler = new SendMessageCommandHandler(this.chatRepository); - } - return this._sendMessageCommandHandler; - } - - public get archiveChatCommandHandler(): ArchiveChatCommandHandler { - if (!this._archiveChatCommandHandler) { - this._archiveChatCommandHandler = new ArchiveChatCommandHandler(this.chatRepository); - } - return this._archiveChatCommandHandler; - } - - public get restoreChatCommandHandler(): RestoreChatCommandHandler { - if (!this._restoreChatCommandHandler) { - this._restoreChatCommandHandler = new RestoreChatCommandHandler(this.chatRepository); - } - return this._restoreChatCommandHandler; - } - - public get createDeckCommandHandler(): CreateDeckCommandHandler { - if (!this._createDeckCommandHandler) { - this._createDeckCommandHandler = new CreateDeckCommandHandler( - this.deckRepository, - this.userRepository, - this.organizationRepository - ); - } - return this._createDeckCommandHandler; - } - - public get updateDeckCommandHandler(): UpdateDeckCommandHandler { - if (!this._updateDeckCommandHandler) { - this._updateDeckCommandHandler = new UpdateDeckCommandHandler(this.deckRepository); - } - return this._updateDeckCommandHandler; - } - - public get deleteDeckCommandHandler(): DeleteDeckCommandHandler { - if (!this._deleteDeckCommandHandler) { - this._deleteDeckCommandHandler = new DeleteDeckCommandHandler(this.deckRepository); - } - return this._deleteDeckCommandHandler; - } - - public get createOrganizationCommandHandler(): CreateOrganizationCommandHandler { - if (!this._createOrganizationCommandHandler) { - this._createOrganizationCommandHandler = new CreateOrganizationCommandHandler(this.organizationRepository); - } - return this._createOrganizationCommandHandler; - } - - public get updateOrganizationCommandHandler(): UpdateOrganizationCommandHandler { - if (!this._updateOrganizationCommandHandler) { - this._updateOrganizationCommandHandler = new UpdateOrganizationCommandHandler(this.organizationRepository); - } - return this._updateOrganizationCommandHandler; - } - - public get deleteOrganizationCommandHandler(): DeleteOrganizationCommandHandler { - if (!this._deleteOrganizationCommandHandler) { - this._deleteOrganizationCommandHandler = new DeleteOrganizationCommandHandler(this.organizationRepository); - } - return this._deleteOrganizationCommandHandler; - } - - public get processOrgAuthCallbackCommandHandler(): ProcessOrgAuthCallbackCommandHandler { - if (!this._processOrgAuthCallbackCommandHandler) { - this._processOrgAuthCallbackCommandHandler = new ProcessOrgAuthCallbackCommandHandler(this.userRepository, this.organizationRepository); - } - return this._processOrgAuthCallbackCommandHandler; - } - - public get createContactCommandHandler(): CreateContactCommandHandler { - if (!this._createContactCommandHandler) { - this._createContactCommandHandler = new CreateContactCommandHandler(this.contactRepository); - } - return this._createContactCommandHandler; - } - - public get updateContactCommandHandler(): UpdateContactCommandHandler { - if (!this._updateContactCommandHandler) { - this._updateContactCommandHandler = new UpdateContactCommandHandler(this.contactRepository); - } - return this._updateContactCommandHandler; - } - - public get deleteContactCommandHandler(): DeleteContactCommandHandler { - if (!this._deleteContactCommandHandler) { - this._deleteContactCommandHandler = new DeleteContactCommandHandler(this.contactRepository); - } - return this._deleteContactCommandHandler; - } - - public get generateBoardCommandHandler(): GenerateBoardCommandHandler { - if (!this._generateBoardCommandHandler) { - this._generateBoardCommandHandler = new GenerateBoardCommandHandler(this.boardGenerationService, RedisService.getInstance()); - } - return this._generateBoardCommandHandler; - } - - // Query Handler getters - public get getUserByIdQueryHandler(): GetUserByIdQueryHandler { - if (!this._getUserByIdQueryHandler) { - this._getUserByIdQueryHandler = new GetUserByIdQueryHandler(this.userRepository); - } - return this._getUserByIdQueryHandler; - } - - public get getUserChatsQueryHandler(): GetUserChatsQueryHandler { - if (!this._getUserChatsQueryHandler) { - this._getUserChatsQueryHandler = new GetUserChatsQueryHandler(this.chatRepository, this.chatArchiveRepository); - } - return this._getUserChatsQueryHandler; - } - - public get getChatHistoryQueryHandler(): GetChatHistoryQueryHandler { - if (!this._getChatHistoryQueryHandler) { - this._getChatHistoryQueryHandler = new GetChatHistoryQueryHandler(this.chatRepository, this.chatArchiveRepository); - } - return this._getChatHistoryQueryHandler; - } - - public get getArchivedChatsQueryHandler(): GetArchivedChatsQueryHandler { - if (!this._getArchivedChatsQueryHandler) { - this._getArchivedChatsQueryHandler = new GetArchivedChatsQueryHandler(this.chatArchiveRepository); - } - return this._getArchivedChatsQueryHandler; - } - - public get getDeckByIdQueryHandler(): GetDeckByIdQueryHandler { - if (!this._getDeckByIdQueryHandler) { - this._getDeckByIdQueryHandler = new GetDeckByIdQueryHandler(this.deckRepository); - } - return this._getDeckByIdQueryHandler; - } - - public get getOrganizationByIdQueryHandler(): GetOrganizationByIdQueryHandler { - if (!this._getOrganizationByIdQueryHandler) { - this._getOrganizationByIdQueryHandler = new GetOrganizationByIdQueryHandler(this.organizationRepository); - } - return this._getOrganizationByIdQueryHandler; - } - - public get getOrganizationLoginUrlQueryHandler(): GetOrganizationLoginUrlQueryHandler { - if (!this._getOrganizationLoginUrlQueryHandler) { - this._getOrganizationLoginUrlQueryHandler = new GetOrganizationLoginUrlQueryHandler(this.organizationRepository); - } - return this._getOrganizationLoginUrlQueryHandler; - } - - public get getContactByIdQueryHandler(): GetContactByIdQueryHandler { - if (!this._getContactByIdQueryHandler) { - this._getContactByIdQueryHandler = new GetContactByIdQueryHandler(this.contactRepository); - } - return this._getContactByIdQueryHandler; - } - - public get getContactsByPageQueryHandler(): GetContactsByPageQueryHandler { - if (!this._getContactsByPageQueryHandler) { - this._getContactsByPageQueryHandler = new GetContactsByPageQueryHandler(this.contactRepository); - } - return this._getContactsByPageQueryHandler; - } - - // New paginated query handlers - public get getUsersByPageQueryHandler(): GetUsersByPageQueryHandler { - if (!this._getUsersByPageQueryHandler) { - this._getUsersByPageQueryHandler = new GetUsersByPageQueryHandler(this.userRepository); - } - return this._getUsersByPageQueryHandler; - } - - public get getDecksByPageQueryHandler(): GetDecksByPageQueryHandler { - if (!this._getDecksByPageQueryHandler) { - this._getDecksByPageQueryHandler = new GetDecksByPageQueryHandler(this.deckRepository); - } - return this._getDecksByPageQueryHandler; - } - - public get getOrganizationsByPageQueryHandler(): GetOrganizationsByPageQueryHandler { - if (!this._getOrganizationsByPageQueryHandler) { - this._getOrganizationsByPageQueryHandler = new GetOrganizationsByPageQueryHandler(this.organizationRepository); - } - return this._getOrganizationsByPageQueryHandler; - } - - public get getChatsByPageQueryHandler(): GetChatsByPageQueryHandler { - if (!this._getChatsByPageQueryHandler) { - this._getChatsByPageQueryHandler = new GetChatsByPageQueryHandler(this.chatRepository); - } - return this._getChatsByPageQueryHandler; - } -} - -// Export singleton instance -export const container = DIContainer.getInstance(); diff --git a/SerpentRace_Backend/src/Application/Services/DeckImportExportService.ts b/SerpentRace_Backend/src/Application/Services/DeckImportExportService.ts deleted file mode 100644 index 3d3f1978..00000000 --- a/SerpentRace_Backend/src/Application/Services/DeckImportExportService.ts +++ /dev/null @@ -1,208 +0,0 @@ -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import { DeckAggregate, State, CType } from '../../Domain/Deck/DeckAggregate'; -import { IDeckRepository } from '../../Domain/IRepository/IDeckRepository'; -import { logError, logAuth } from './Logger'; - -export interface SprDeckData { - name: string; - type: number; - cards: any[]; - ctype: number; - exportDate: string; - version: string; -} - -export interface ImportDeckCommand { - name: string; - type: number; - cards: any[]; - ctype?: number; - userid: string; -} - -export class DeckImportExportService { - private readonly encryptionKey: string; - private readonly algorithm = 'aes-256-gcm'; - - constructor(private readonly deckRepo: IDeckRepository) { - this.encryptionKey = process.env.DECK_ENCRYPTION_KEY || 'your-32-byte-encryption-key-here!!'; - - if (this.encryptionKey.length !== 32) { - throw new Error('DECK_ENCRYPTION_KEY must be exactly 32 characters long'); - } - } - - async exportDeckToSpr(deckId: string, userId: string): Promise { - try { - const deck = await this.deckRepo.findByIdIncludingDeleted(deckId); - - if (!deck) { - throw new Error('Deck not found'); - } - - if (deck.userid !== userId) { - throw new Error('Unauthorized: You can only export your own decks'); - } - - const deckData: SprDeckData = { - name: deck.name, - type: deck.type, - cards: deck.cards, - ctype: deck.ctype, - exportDate: new Date().toISOString(), - version: '1.0' - }; - - const jsonString = JSON.stringify(deckData); - const encrypted = this.encrypt(jsonString); - - logAuth('Deck exported to SPR format', userId, { - deckId: deck.id, - deckName: deck.name, - cardCount: deck.cards.length - }); - - return encrypted; - } catch (error) { - logError('Failed to export deck to SPR', error as Error); - throw error; - } - } - - async importDeckFromSpr(sprData: Buffer, userId: string): Promise { - try { - const decrypted = this.decrypt(sprData); - const deckData: SprDeckData = JSON.parse(decrypted); - - // Validate required fields - if (!deckData.name || !deckData.cards || deckData.type === undefined) { - throw new Error('Invalid SPR file format: missing required fields'); - } - - // Create new deck - const newDeck = new DeckAggregate(); - newDeck.name = deckData.name; - newDeck.type = deckData.type; - newDeck.userid = userId; - newDeck.cards = deckData.cards; - newDeck.ctype = deckData.ctype || CType.PUBLIC; - newDeck.state = State.ACTIVE; - - const createdDeck = await this.deckRepo.create(newDeck); - - logAuth('Deck imported from SPR format', userId, { - deckId: createdDeck.id, - deckName: createdDeck.name, - cardCount: createdDeck.cards.length, - originalExportDate: deckData.exportDate - }); - - return createdDeck; - } catch (error) { - logError('Failed to import deck from SPR', error as Error); - throw error; - } - } - - async importDeckFromJson(jsonData: any, userId: string): Promise { - try { - // Validate required fields - if (!jsonData.name || !jsonData.cards || jsonData.type === undefined) { - throw new Error('Invalid JSON format: missing required fields (name, cards, type)'); - } - - // Create new deck - const newDeck = new DeckAggregate(); - newDeck.name = jsonData.name; - newDeck.type = jsonData.type; - newDeck.userid = userId; - newDeck.cards = jsonData.cards; - newDeck.ctype = jsonData.ctype || CType.PUBLIC; - newDeck.state = State.ACTIVE; - - const createdDeck = await this.deckRepo.create(newDeck); - - logAuth('Deck imported from JSON format', userId, { - deckId: createdDeck.id, - deckName: createdDeck.name, - cardCount: createdDeck.cards.length - }); - - return createdDeck; - } catch (error) { - logError('Failed to import deck from JSON', error as Error); - throw error; - } - } - - // Admin-only function to import JSON without encryption - async adminImportFromJson(jsonData: any, targetUserId: string, adminUserId: string): Promise { - try { - if (!jsonData.name || !jsonData.cards || jsonData.type === undefined) { - throw new Error('Invalid JSON format: missing required fields (name, cards, type)'); - } - - const newDeck = new DeckAggregate(); - newDeck.name = jsonData.name; - newDeck.type = jsonData.type; - newDeck.userid = targetUserId; - newDeck.cards = jsonData.cards; - newDeck.ctype = jsonData.ctype || CType.PUBLIC; - newDeck.state = jsonData.state || State.ACTIVE; - - const createdDeck = await this.deckRepo.create(newDeck); - - logAuth('Deck imported by admin from JSON', adminUserId, { - deckId: createdDeck.id, - deckName: createdDeck.name, - cardCount: createdDeck.cards.length, - targetUserId: targetUserId - }); - - return createdDeck; - } catch (error) { - logError('Failed to admin import deck from JSON', error as Error); - throw error; - } - } - - private encrypt(text: string): Buffer { - const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv(this.algorithm, this.encryptionKey, iv); - cipher.setAAD(Buffer.from('SerpentRace-Deck', 'utf8')); - - let encrypted = cipher.update(text, 'utf8'); - encrypted = Buffer.concat([encrypted, cipher.final()]); - - const authTag = cipher.getAuthTag(); - - return Buffer.concat([iv, authTag, encrypted]); - } - - private decrypt(encryptedData: Buffer): string { - if (encryptedData.length < 32) { - throw new Error('Invalid SPR file: file too short'); - } - - const iv = encryptedData.slice(0, 16); - const authTag = encryptedData.slice(16, 32); - const encrypted = encryptedData.slice(32); - - const decipher = crypto.createDecipheriv(this.algorithm, this.encryptionKey, iv); - decipher.setAAD(Buffer.from('SerpentRace-Deck', 'utf8')); - decipher.setAuthTag(authTag); - - let decrypted = decipher.update(encrypted, undefined, 'utf8'); - decrypted += decipher.final('utf8'); - - return decrypted; - } - - generateFilename(deckName: string): string { - // Sanitize deck name for filename - const sanitized = deckName.replace(/[^a-zA-Z0-9\-_]/g, '_'); - const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD - return `${sanitized}_${timestamp}.spr`; - } -} diff --git a/SerpentRace_Backend/src/Application/Services/EmailService.ts b/SerpentRace_Backend/src/Application/Services/EmailService.ts deleted file mode 100644 index d4c25396..00000000 --- a/SerpentRace_Backend/src/Application/Services/EmailService.ts +++ /dev/null @@ -1,312 +0,0 @@ -import * as nodemailer from 'nodemailer'; -import * as fs from 'fs'; -import * as path from 'path'; -import sharp from 'sharp'; -import { logError, logAuth, logStartup } from './Logger'; -import { EmailTemplateHelper, LocalizedSubjects } from './EmailTemplateHelper'; - - -export interface EmailOptions { - to: string; - subject: string; - html?: string; - text?: string; - template?: string; - templateData?: any; -} - -export interface EmailConfig { - host: string; - port: number; - secure: boolean; - auth: { - user: string; - pass: string; - }; - from: string; -} - -export class EmailService { - private transporter!: nodemailer.Transporter; - private config: EmailConfig; - private templatesPath: string; - private logoPath: string; - private resizedLogoBuffer?: Buffer; - - constructor() { - this.templatesPath = path.join(__dirname, '../../Templates'); - this.logoPath = path.join(__dirname, '../../../assets/Logo.png'); - // Load logo asynchronously after initialization - this.loadLogo().catch(err => logError('Error loading logo:', err)); - - this.config = { - host: process.env.EMAIL_HOST || 'smtp.gmail.com', - port: parseInt(process.env.EMAIL_PORT || '587'), - secure: process.env.EMAIL_SECURE === 'true', - auth: { - user: process.env.EMAIL_USER || '', - pass: process.env.EMAIL_PASS || '' - }, - from: process.env.EMAIL_FROM || 'noreply@serpentrace.com' - }; - - this.initializeTransporter(); - } - - private initializeTransporter(): void { - try { - this.transporter = nodemailer.createTransport({ - host: this.config.host, - port: this.config.port, - secure: this.config.secure, - auth: { - user: this.config.auth.user, - pass: this.config.auth.pass - } - }); - } catch (error) { - logError('EmailService initialization failed', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to initialize email service'); - } - } - - /** - * Load and resize logo for email attachments - 60x60 pixels - */ - private async loadLogo(): Promise { - try { - if (fs.existsSync(this.logoPath)) { - const logoBuffer = fs.readFileSync(this.logoPath); - - // Resize to 60x60 pixels with high quality and centered - this.resizedLogoBuffer = await sharp(logoBuffer) - .resize(60, 60, { - fit: 'contain', - background: { r: 255, g: 255, b: 255, alpha: 1 }, - position: 'center' - }) - .png() - .toBuffer(); - } - } catch (error) { - logError('Failed to load logo for emails', error instanceof Error ? error : new Error(String(error))); - } - } - - /** - * Send email with template - * @param options - Email options including template and data - */ - async sendEmail(options: EmailOptions): Promise { - try { - // Ensure logo is loaded before sending - if (!this.resizedLogoBuffer) { - await this.loadLogo(); - } - - let htmlContent = options.html; - let textContent = options.text; - - if (options.template) { - const templateResult = await this.loadTemplate(options.template, options.templateData); - htmlContent = templateResult.html; - textContent = templateResult.text; - } - - const mailOptions: any = { - from: this.config.from, - to: options.to, - subject: options.subject, - html: htmlContent, - text: textContent, - attachments: [] - }; - - // Add logo as CID attachment if available - if (this.resizedLogoBuffer) { - mailOptions.attachments.push({ - filename: 'logo.png', - content: this.resizedLogoBuffer, - cid: 'logo@serpentrace' // Content-ID for referencing in HTML - }); - console.log('[EmailService] 📎 Logo attached to email as CID: logo@serpentrace'); - } else { - console.warn('[EmailService] ⚠️ Logo buffer not available, email will be sent without logo'); - } - - const result = await this.transporter.sendMail(mailOptions); - logAuth('Email sent successfully', undefined, { - messageId: result.messageId, - to: options.to, - subject: options.subject - }); - return true; - } catch (error) { - logError('Email sending failed', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Send verification email to user - * @param userEmail - User's email address - * @param userName - User's name - * @param verificationToken - Verification token - * @param verificationUrl - Complete verification URL - * @param language - Language code ('en', 'hu', 'de') - */ - async sendVerificationEmail( - userEmail: string, - userName: string, - verificationToken: string, - verificationUrl: string, - language: 'en' | 'hu' | 'de' = 'en' - ): Promise { - try { - const templateName = language === 'en' ? 'verification' : `verification-${language}`; - const subject = this.getLocalizedVerificationSubject(language); - - return await this.sendEmail({ - to: userEmail, - subject, - template: templateName, - templateData: { - userName, - verificationToken, - verificationUrl, - companyName: 'SerpentRace', - supportEmail: 'support@serpentrace.com' - } - }); - } catch (error) { - logError('Verification email sending failed', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Send password reset email - * @param userEmail - User's email address - * @param userName - User's name - * @param resetToken - Password reset token - * @param resetUrl - Complete password reset URL - * @param language - Language code ('en', 'hu', 'de') - */ - async sendPasswordResetEmail( - userEmail: string, - userName: string, - resetToken: string, - resetUrl: string, - language: 'en' | 'hu' | 'de' = 'en' - ): Promise { - try { - const templateName = language === 'en' ? 'password-reset' : `password-reset-${language}`; - const subject = this.getLocalizedPasswordResetSubject(language); - - return await this.sendEmail({ - to: userEmail, - subject, - template: templateName, - templateData: { - userName, - resetToken, - resetUrl, - companyName: 'SerpentRace', - supportEmail: 'support@serpentrace.com' - } - }); - } catch (error) { - logError('Password reset email sending failed', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Load and compile email template with language support - * @param templateName - Name of the template file (with or without language suffix) - * @param data - Data to replace placeholders in the template - */ - private async loadTemplate(templateName: string, data: any): Promise<{ html: string; text: string }> { - try { - // Try the specified template first - let htmlTemplatePath = path.join(this.templatesPath, `${templateName}.html`); - let textTemplatePath = path.join(this.templatesPath, `${templateName}.txt`); - - let htmlTemplate = ''; - let textTemplate = ''; - - // Load HTML template if it exists - if (fs.existsSync(htmlTemplatePath)) { - htmlTemplate = fs.readFileSync(htmlTemplatePath, 'utf8'); - } else { - // If language-specific template doesn't exist, try fallback to English - const baseName = templateName.replace(/-[a-z]{2}$/, ''); // Remove language suffix - const fallbackHtmlPath = path.join(this.templatesPath, `${baseName}.html`); - if (fs.existsSync(fallbackHtmlPath)) { - htmlTemplate = fs.readFileSync(fallbackHtmlPath, 'utf8'); - } - } - - // Load text template if it exists - if (fs.existsSync(textTemplatePath)) { - textTemplate = fs.readFileSync(textTemplatePath, 'utf8'); - } else { - // If language-specific template doesn't exist, try fallback to English - const baseName = templateName.replace(/-[a-z]{2}$/, ''); // Remove language suffix - const fallbackTextPath = path.join(this.templatesPath, `${baseName}.txt`); - if (fs.existsSync(fallbackTextPath)) { - textTemplate = fs.readFileSync(fallbackTextPath, 'utf8'); - } - } - - // If no templates found, throw error - if (!htmlTemplate && !textTemplate) { - throw new Error(`Template '${templateName}' not found`); - } - - // Replace placeholders in templates - const processedTemplate = EmailTemplateHelper.processTemplate( - { html: htmlTemplate, text: textTemplate }, - data - ); - - return { - html: processedTemplate.html, - text: processedTemplate.text - }; - } catch (error) { - logError('Email template loading failed', error instanceof Error ? error : new Error(String(error))); - throw new Error(`Failed to load email template: ${templateName}`); - } - } - - /** - * Get localized verification email subject - * @param language - Language code ('en', 'hu', 'de') - */ - private getLocalizedVerificationSubject(language: 'en' | 'hu' | 'de'): string { - const subjects: LocalizedSubjects = { - verification: { - en: 'SerpentRace - Verify Your Account', - hu: 'SerpentRace - Fiók megerősítése', - de: 'SerpentRace - Konto verifizieren' - } - }; - return EmailTemplateHelper.getLocalizedSubject('verification', subjects, language); - } - - /** - * Get localized password reset email subject - * @param language - Language code ('en', 'hu', 'de') - */ - private getLocalizedPasswordResetSubject(language: 'en' | 'hu' | 'de'): string { - const subjects: LocalizedSubjects = { - passwordReset: { - en: 'SerpentRace - Password Reset Request', - hu: 'SerpentRace - Jelszó visszaállítás kérése', - de: 'SerpentRace - Passwort zurücksetzen' - } - }; - return EmailTemplateHelper.getLocalizedSubject('passwordReset', subjects, language); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/EmailTemplateHelper.ts b/SerpentRace_Backend/src/Application/Services/EmailTemplateHelper.ts deleted file mode 100644 index d58a1784..00000000 --- a/SerpentRace_Backend/src/Application/Services/EmailTemplateHelper.ts +++ /dev/null @@ -1,39 +0,0 @@ -export interface LocalizedSubjects { - [key: string]: { - en: string; - hu: string; - de: string; - }; -} - -export interface TemplateData { - [key: string]: any; -} - -export interface EmailTemplate { - html: string; - text: string; -} - -export class EmailTemplateHelper { - public static getLocalizedSubject( - subjectKey: string, - subjects: LocalizedSubjects, - language: 'en' | 'hu' | 'de' - ): string { - return subjects[subjectKey]?.[language] || subjects[subjectKey]?.['en'] || 'SerpentRace'; - } - - public static replaceTemplatePlaceholders(template: string, data: TemplateData): string { - return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => { - return data[key] !== undefined ? String(data[key]) : match; - }); - } - - public static processTemplate(templateContent: EmailTemplate, data: TemplateData): EmailTemplate { - return { - html: this.replaceTemplatePlaceholders(templateContent.html, data), - text: this.replaceTemplatePlaceholders(templateContent.text, data) - }; - } -} diff --git a/SerpentRace_Backend/src/Application/Services/ErrorResponseService.ts b/SerpentRace_Backend/src/Application/Services/ErrorResponseService.ts deleted file mode 100644 index e4614f42..00000000 --- a/SerpentRace_Backend/src/Application/Services/ErrorResponseService.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Response } from 'express'; - -export class ErrorResponseService { - static sendError(res: Response, statusCode: number, message: string, details?: any): Response { - const errorResponse: any = { error: message }; - if (details) { - errorResponse.details = details; - } - return res.status(statusCode).json(errorResponse); - } - - static sendInternalServerError(res: Response): Response { - return this.sendError(res, 500, 'Internal server error'); - } - - static sendBadRequest(res: Response, message: string = 'Bad request', details?: any): Response { - return this.sendError(res, 400, message, details); - } - - static sendUnauthorized(res: Response, message: string = 'Unauthorized'): Response { - return this.sendError(res, 401, message); - } - - static sendForbidden(res: Response, message: string = 'Forbidden'): Response { - return this.sendError(res, 403, message); - } - - static sendNotFound(res: Response, message: string = 'Not found'): Response { - return this.sendError(res, 404, message); - } - - static sendConflict(res: Response, message: string = 'Conflict'): Response { - return this.sendError(res, 409, message); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/FieldEffectService.ts b/SerpentRace_Backend/src/Application/Services/FieldEffectService.ts deleted file mode 100644 index 22039a4d..00000000 --- a/SerpentRace_Backend/src/Application/Services/FieldEffectService.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { GameCard, GameField } from '../../Domain/Game/GameAggregate'; -import { Consequence } from '../../Domain/Deck/DeckAggregate'; -import { BoardGenerationService } from '../Game/BoardGenerationService'; -import { GamemasterService, GamemasterDecisionResult } from './GamemasterService'; -import { FieldEffectRequest, FieldEffectResult } from './Interfaces/GameInterfaces'; - -// Interfaces for different card processing results -export interface GuessResult { - guessedPosition: number; - actualPosition: number; - isCorrect: boolean; - penaltyApplied: boolean; // true if moved back 2 fields - description: string; -} - -export interface TurnEffect { - type: 'LOSE_TURN' | 'EXTRA_TURN'; - playerId: string; - value: number; // Number of turns to lose/gain -} - -export interface CardProcessingResult { - finalPosition: number; - stepValue: number; - dice: number; - patternModifier: number; - consequenceModifier: number; - guessResult?: GuessResult; - gamemasterResult?: GamemasterDecisionResult; - turnEffect?: TurnEffect; // Turn-based consequences that need game state changes - description: string; - effects: string[]; // Array of all effects applied -} - -/** - * Service responsible for processing card-based field effects with step calculations - * Integrates pattern-based movement with test/guess mechanism and gamemaster decisions - */ -export class FieldEffectService { - constructor( - private boardGenerationService: BoardGenerationService, - private gamemasterService: GamemasterService - ) {} - - /** - * Process a card-based field effect with complete movement calculation - * @param request The field effect request containing all necessary data - * @returns Promise with complete processing result - */ - async processFieldEffect(request: FieldEffectRequest): Promise { - const { currentPosition, card, field, dice } = request; - - // Ensure stepValue is defined - const stepValue = field.stepValue || 1; - - // Calculate base movement using pattern-based system - const finalPosition = this.boardGenerationService.calculatePatternBasedMovement( - currentPosition, - stepValue, - dice - ); - - // Calculate pattern modifier manually for tracking - const patternModifier = this.getPatternModifier(currentPosition); - - let result: CardProcessingResult = { - finalPosition, - stepValue, - dice, - patternModifier, - consequenceModifier: 0, - description: `Moved from position ${currentPosition} to ${finalPosition}`, - effects: [] - }; - - // Process card based on type - if (this.isQuestionCard(card.type)) { - result = await this.processQuestionCard(request, result); - } else if (this.isJokerCard(card.type)) { - result = await this.processJokerCard(request, result); - } else if (this.isLuckCard(card.type)) { - result = await this.processLuckCard(request, result); - } - - return result; - } - - /** - * Get pattern modifier based on position (duplicated from BoardGenerationService) - * @param position Current position - * @returns Pattern modifier value - */ - private getPatternModifier(position: number): number { - // Pattern modifiers for strategic complexity: - // Positions 1-20: +2 bonus (easier start) - // Positions 21-40: -1 penalty (early game challenge) - // Positions 41-60: +1 bonus (mid-game boost) - // Positions 61-80: -2 penalty (late game challenge) - // Positions 81-100: +3 bonus (final stretch boost) - - if (position <= 20) { - return 2; - } else if (position <= 40) { - return -1; - } else if (position <= 60) { - return 1; - } else if (position <= 80) { - return -2; - } else { - return 3; - } - } - - /** - * Check if card is a question card (types 0-4) - * @param cardType Card type - * @returns True if question card - */ - private isQuestionCard(cardType?: number): boolean { - return cardType !== undefined && cardType >= 0 && cardType <= 4; - } - - /** - * Check if card is a joker card - * @param cardType Card type - * @returns True if joker card - */ - private isJokerCard(cardType?: number): boolean { - return cardType === 5; // Assuming joker cards have type 5 - } - - /** - * Check if card is a luck card - * @param cardType Card type - * @returns True if luck card - */ - private isLuckCard(cardType?: number): boolean { - return cardType === 6; // Assuming luck cards have type 6 - } - - /** - * Process question card with test/guess mechanism - * @param request The field effect request - * @param baseResult The base movement calculation result - * @returns Updated result with guess processing - */ - private async processQuestionCard( - request: FieldEffectRequest, - baseResult: CardProcessingResult - ): Promise { - const { guessedPosition } = request; - - if (guessedPosition === undefined) { - throw new Error('Question cards require a position guess'); - } - - // Apply test/guess mechanism - const guessResult = this.processGuess( - guessedPosition, - baseResult.finalPosition, - baseResult.finalPosition - ); - - let finalPosition = baseResult.finalPosition; - let effects = [...baseResult.effects]; - - if (!guessResult.isCorrect) { - // Apply guess penalty: move back exactly 2 fields - finalPosition = Math.max(1, baseResult.finalPosition - 2); - effects.push(`Wrong guess penalty: moved back 2 fields`); - } else { - effects.push(`Correct guess: no penalty`); - } - - return { - ...baseResult, - finalPosition, - guessResult, - effects, - description: `Question card: ${guessResult.description}` - }; - } - - /** - * Process joker card with same guess mechanism as question cards + gamemaster decision - * @param request The field effect request - * @param baseResult The base movement calculation result - * @returns Updated result with guess processing and gamemaster decision - */ - private async processJokerCard( - request: FieldEffectRequest, - baseResult: CardProcessingResult - ): Promise { - const { guessedPosition, gameId, playerId, playerName, card } = request; - - if (guessedPosition === undefined) { - throw new Error('Joker cards require a position guess'); - } - - // Joker cards always use dice = 6, recalculate with correct dice value - const jokerDice = 6; - const correctBasePosition = this.boardGenerationService.calculatePatternBasedMovement( - request.currentPosition, - baseResult.stepValue, - jokerDice - ); - - let finalPosition = correctBasePosition; - let effects = [`Joker card: dice counted as 6`]; - - // Step 1: Process guess penalty (same as question cards) - const guessResult = this.processGuess( - guessedPosition, - correctBasePosition, - correctBasePosition - ); - - if (!guessResult.isCorrect) { - // Apply guess penalty: move back exactly 2 fields - finalPosition = Math.max(1, correctBasePosition - 2); - effects.push(`Wrong guess penalty: moved back 2 fields`); - } else { - effects.push(`Correct guess: no penalty`); - } - - // Step 2: Process gamemaster decision (replaces player answer effect) - const gamemasterResult = await this.requestGamemasterDecision( - gameId, - playerId, - playerName, - card - ); - - let consequenceModifier = 0; - let turnEffect: TurnEffect | undefined; - - if (gamemasterResult.consequence && card.consequence) { - // Apply consequence based on gamemaster decision using new processing method - const consequenceResult = this.processConsequence(playerId, finalPosition, card.consequence); - finalPosition = consequenceResult.newPosition; - consequenceModifier = consequenceResult.positionChange; - turnEffect = consequenceResult.turnEffect; - - effects.push(`Gamemaster decision: ${gamemasterResult.description}`); - effects.push(`Consequence applied: ${this.getConsequenceDescription(card.consequence)}`); - } else { - effects.push(`No consequence applied: ${gamemasterResult.description}`); - } - - return { - ...baseResult, - finalPosition, - dice: jokerDice, // Update to show dice was 6 - consequenceModifier, - turnEffect, - guessResult, - gamemasterResult, - effects, - description: `Joker card: ${guessResult.description} | ${gamemasterResult.description}` - }; - } - - /** - * Process luck card with immediate effects - * @param request The field effect request - * @param baseResult The base movement calculation result - * @returns Updated result with luck card effects - */ - private async processLuckCard( - request: FieldEffectRequest, - baseResult: CardProcessingResult - ): Promise { - const { card, playerId } = request; - - let finalPosition = baseResult.finalPosition; - let consequenceModifier = 0; - let turnEffect: TurnEffect | undefined; - let effects = [...baseResult.effects]; - - if (card.consequence) { - // Apply immediate consequence using new processing method - const consequenceResult = this.processConsequence(playerId, finalPosition, card.consequence); - finalPosition = consequenceResult.newPosition; - consequenceModifier = consequenceResult.positionChange; - turnEffect = consequenceResult.turnEffect; - effects.push(`Luck effect: ${this.getConsequenceDescription(card.consequence)}`); - } - - return { - ...baseResult, - finalPosition, - consequenceModifier, - turnEffect, - effects, - description: `Luck card: immediate effect applied` - }; - } - - /** - * Process position guess and determine if penalty should be applied - * @param guessedPosition Player's position guess - * @param actualPosition The calculated final position - * @param basePosition The position before guess penalty - * @returns Guess processing result - */ - private processGuess( - guessedPosition: number, - actualPosition: number, - basePosition: number - ): GuessResult { - // Validate guess range - if (guessedPosition < 1 || guessedPosition > 100) { - throw new Error('Position guess must be between 1 and 100'); - } - - const isCorrect = guessedPosition === actualPosition; - const penaltyApplied = !isCorrect; - - return { - guessedPosition, - actualPosition, - isCorrect, - penaltyApplied, - description: isCorrect - ? `Correct guess (${guessedPosition})!` - : `Wrong guess (${guessedPosition} ≠ ${actualPosition})` - }; - } - - /** - * Request gamemaster decision for joker card - * @param gameId Game ID - * @param playerId Player ID - * @param playerName Player name - * @param card Joker card - * @returns Promise with gamemaster decision result - */ - private async requestGamemasterDecision( - gameId: string, - playerId: string, - playerName: string, - card: GameCard - ): Promise { - // For now, return a default decision - this will be replaced with actual async gamemaster interaction - // TODO: Implement proper WebSocket-based gamemaster decision flow - return { - decision: 'reject' as any, - consequence: false, - description: '🎭 Gamemaster decision pending...' - }; - } - - /** - * Process consequence and separate position changes from turn effects - * @param playerId Player ID who drew the card - * @param currentPosition Current position before consequence - * @param consequence Card consequence - * @returns Object with position changes and turn effects - */ - private processConsequence(playerId: string, currentPosition: number, consequence: Consequence): { - newPosition: number; - positionChange: number; - turnEffect?: TurnEffect; - } { - // Handle position-affecting consequences - if (consequence.type === 0 || consequence.type === 1 || consequence.type === 5) { - const newPosition = this.applyConsequenceToPosition(currentPosition, consequence); - return { - newPosition, - positionChange: newPosition - currentPosition - }; - } - - // Handle turn-based consequences - if (consequence.type === 2 || consequence.type === 3) { - const turnEffect: TurnEffect = { - type: consequence.type === 2 ? 'LOSE_TURN' : 'EXTRA_TURN', - playerId, - value: consequence.value || 1 - }; - return { - newPosition: currentPosition, // No position change - positionChange: 0, - turnEffect - }; - } - - // Unknown consequence type - return { - newPosition: currentPosition, - positionChange: 0 - }; - } - - /** - * Apply consequence to position with proper boundary handling - * @param currentPosition Current position - * @param consequence Card consequence - * @returns New position after consequence - */ - private applyConsequenceToPosition(currentPosition: number, consequence: Consequence): number { - switch (consequence.type) { - case 0: // MOVE_FORWARD - return Math.min(100, currentPosition + (consequence.value || 1)); - case 1: // MOVE_BACKWARD - return Math.max(1, currentPosition - (consequence.value || 1)); - case 5: // GO_TO_START - return 1; - default: - return currentPosition; // Other consequences don't change position - } - } - - /** - * Get human-readable description for consequence - * @param consequence Card consequence - * @returns Description string - */ - private getConsequenceDescription(consequence: Consequence): string { - switch (consequence.type) { - case 0: // MOVE_FORWARD - return `Move forward ${consequence.value || 1} steps`; - case 1: // MOVE_BACKWARD - return `Move backward ${consequence.value || 1} steps`; - case 2: // LOSE_TURN - const lostTurns = consequence.value || 1; - return lostTurns === 1 ? 'Lose next turn' : `Lose next ${lostTurns} turns`; - case 3: // EXTRA_TURN - const extraTurns = consequence.value || 1; - return extraTurns === 1 ? 'Get extra turn' : `Get ${extraTurns} extra turns`; - case 5: // GO_TO_START - return 'Go back to start'; - default: - return 'Unknown effect'; - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/GameSnapshotService.ts b/SerpentRace_Backend/src/Application/Services/GameSnapshotService.ts deleted file mode 100644 index e208485f..00000000 --- a/SerpentRace_Backend/src/Application/Services/GameSnapshotService.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { IGameSnapshotRepository } from '../../Domain/IRepository/IGameSnapshotRepository'; -import { GameSnapshotAggregate, SnapshotTrigger, GameStateSnapshot, PlayerSnapshot } from '../../Domain/Game/GameSnapshotAggregate'; -import { RedisService } from './RedisService'; -import { logOther, logError } from './Logger'; - -export class GameSnapshotService { - private static readonly SNAPSHOT_INTERVAL = 5; // Every 5 turns - private static readonly MAX_SNAPSHOTS_PER_GAME = 20; // Keep last 20 snapshots - - constructor( - private snapshotRepository: IGameSnapshotRepository, - private redisService: RedisService - ) {} - - /** - * Create a game state snapshot - */ - async createSnapshot( - gameId: string, - turnNumber: number, - trigger: SnapshotTrigger, - notes?: string - ): Promise { - try { - // Gather current game state from Redis - const gameState = await this.getCurrentGameState(gameId); - if (!gameState) { - logError('Cannot create snapshot: game state not found', new Error(`Game ${gameId} not in Redis`)); - return; - } - - // Gather Redis state (pending actions, timers, etc.) - const redisState = await this.getRedisState(gameId); - - // Create snapshot - const snapshot = new GameSnapshotAggregate(); - snapshot.gameid = gameId; - snapshot.turnNumber = turnNumber; - snapshot.trigger = trigger; - snapshot.gameState = gameState; - snapshot.redisState = redisState; - snapshot.notes = notes || null; - - await this.snapshotRepository.save(snapshot); - - // Cleanup old snapshots - await this.snapshotRepository.deleteOldSnapshots( - gameId, - GameSnapshotService.MAX_SNAPSHOTS_PER_GAME - ); - - logOther(`Game snapshot created: ${trigger}`, { - gameId, - turnNumber, - trigger - }); - } catch (error) { - logError('Failed to create game snapshot', error as Error); - // Don't throw - snapshots shouldn't break game flow - } - } - - /** - * Check if snapshot should be created (every N turns) - */ - shouldCreateSnapshot(turnNumber: number): boolean { - return turnNumber % GameSnapshotService.SNAPSHOT_INTERVAL === 0; - } - - /** - * Restore game state from latest snapshot - */ - async restoreFromSnapshot(gameId: string): Promise { - try { - const snapshot = await this.snapshotRepository.findLatestByGameId(gameId); - if (!snapshot) { - logOther(`No snapshot found for game ${gameId}`); - return false; - } - - // Restore game state to Redis - await this.restoreGameState(gameId, snapshot.gameState); - - // Restore Redis state (pending actions, timers) - if (snapshot.redisState) { - await this.restoreRedisState(gameId, snapshot.redisState); - } - - logOther(`Game state restored from snapshot`, { - gameId, - turnNumber: snapshot.turnNumber, - trigger: snapshot.trigger, - age: Date.now() - snapshot.createdat.getTime() - }); - - return true; - } catch (error) { - logError('Failed to restore game from snapshot', error as Error); - return false; - } - } - - /** - * Get current game state from Redis - */ - private async getCurrentGameState(gameId: string): Promise { - try { - // Get game state - const gameStateKey = `game_state:${gameId}`; - const gameStateJson = await this.redisService.get(gameStateKey); - if (!gameStateJson) return null; - - const gameState = JSON.parse(gameStateJson); - - // Get player positions - const playerPositions: PlayerSnapshot[] = []; - const positionsKey = `player_positions:${gameId}`; - const positionsJson = await this.redisService.get(positionsKey); - - if (positionsJson) { - const positions = JSON.parse(positionsJson); - for (const [playerId, data] of Object.entries(positions)) { - const posData = data as any; - - // Get extra turns - const extraTurnsKey = `extra_turns:${gameId}:${playerId}`; - const extraTurns = parseInt(await this.redisService.get(extraTurnsKey) || '0'); - - // Get turns to lose - const turnsToLoseKey = `turns_to_lose:${gameId}:${playerId}`; - const turnsToLose = parseInt(await this.redisService.get(turnsToLoseKey) || '0'); - - playerPositions.push({ - playerId: playerId, - playerName: posData.playerName || 'Unknown', - boardPosition: posData.boardPosition || 0, - extraTurns, - turnsToLose, - isOnline: posData.isOnline !== false - }); - } - } - - // Get board data - const boardKey = `board_data:${gameId}`; - const boardJson = await this.redisService.get(boardKey); - const boardFields = boardJson ? JSON.parse(boardJson).fields : undefined; - - return { - currentPlayer: gameState.currentPlayer, - currentPlayerName: gameState.currentPlayerName || 'Unknown', - turnNumber: gameState.turnNumber || 1, - turnOrder: gameState.turnOrder || [], - playerPositions, - boardFields, - deckStates: undefined, // TODO: Add deck states if needed - pendingActions: undefined - }; - } catch (error) { - logError('Error getting current game state', error as Error); - return null; - } - } - - /** - * Get Redis state (pending cards, decisions, etc.) - */ - private async getRedisState(gameId: string): Promise { - const redisState: any = { - pendingCards: {}, - pendingDecisions: {}, - timers: {} - }; - - try { - // Get all keys for this game - const pattern = `*${gameId}*`; - const keys = await this.redisService['client'].keys(pattern); - - for (const key of keys) { - // Store non-critical state for reference - if (key.includes('pending_card') || key.includes('pending_decision')) { - const value = await this.redisService.get(key); - if (value) { - redisState.pendingCards[key] = value; - } - } - } - } catch (error) { - logError('Error getting Redis state', error as Error); - } - - return redisState; - } - - /** - * Restore game state to Redis - */ - private async restoreGameState(gameId: string, state: GameStateSnapshot): Promise { - // Restore game state - const gameStateKey = `game_state:${gameId}`; - await this.redisService.setWithExpiry(gameStateKey, JSON.stringify({ - currentPlayer: state.currentPlayer, - currentPlayerName: state.currentPlayerName, - turnNumber: state.turnNumber, - turnOrder: state.turnOrder - }), 3600); - - // Restore player positions - const positionsKey = `player_positions:${gameId}`; - const positions: any = {}; - for (const player of state.playerPositions) { - positions[player.playerId] = { - playerName: player.playerName, - boardPosition: player.boardPosition, - isOnline: player.isOnline - }; - - // Restore extra turns - if (player.extraTurns > 0) { - const extraTurnsKey = `extra_turns:${gameId}:${player.playerId}`; - await this.redisService.setWithExpiry(extraTurnsKey, player.extraTurns.toString(), 3600); - } - - // Restore turns to lose - if (player.turnsToLose > 0) { - const turnsToLoseKey = `turns_to_lose:${gameId}:${player.playerId}`; - await this.redisService.setWithExpiry(turnsToLoseKey, player.turnsToLose.toString(), 3600); - } - } - await this.redisService.setWithExpiry(positionsKey, JSON.stringify(positions), 3600); - - // Restore board data if available - if (state.boardFields) { - const boardKey = `board_data:${gameId}`; - await this.redisService.setWithExpiry(boardKey, JSON.stringify({ fields: state.boardFields }), 3600); - } - } - - /** - * Restore Redis state (partial - pending actions may need re-triggering) - */ - private async restoreRedisState(gameId: string, redisState: any): Promise { - // Note: Pending cards and timers should be recreated by game logic - // This is just for reference/debugging - logOther('Redis state reference saved (timers/pending actions need manual restart)'); - } - - /** - * Cleanup snapshots for finished game - */ - async cleanupGameSnapshots(gameId: string): Promise { - try { - await this.snapshotRepository.deleteByGameId(gameId); - logOther(`Game snapshots cleaned up for game ${gameId}`); - } catch (error) { - logError('Failed to cleanup game snapshots', error as Error); - } - } - - /** - * Get snapshot history for debugging - */ - async getSnapshotHistory(gameId: string): Promise { - return await this.snapshotRepository.findByGameId(gameId); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/GameTokenService.ts b/SerpentRace_Backend/src/Application/Services/GameTokenService.ts deleted file mode 100644 index c4fca106..00000000 --- a/SerpentRace_Backend/src/Application/Services/GameTokenService.ts +++ /dev/null @@ -1,205 +0,0 @@ -import jwt, { SignOptions } from 'jsonwebtoken'; -import { Request } from 'express'; - -export interface GameTokenPayload { - gameId: string; - gameCode: string; - playerName: string; - isAuthenticated: boolean; - userId?: string; // Optional - only for authenticated players - tokenType: 'game_session'; - iat?: number; - exp?: number; -} - -export class GameTokenService { - private readonly secretKey: string; - private readonly gameTokenExpiry: number; - - constructor() { - this.secretKey = process.env.JWT_SECRET || 'your-secret-key'; - - // Game tokens expire after 24 hours (or configured duration) - // This should be longer than typical game duration - this.gameTokenExpiry = parseInt(process.env.GAME_TOKEN_EXPIRY || '86400'); // 24 hours default - - if (process.env.NODE_ENV === 'production' && (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'your-secret-key')) { - throw new Error('JWT_SECRET environment variable must be set in production'); - } - } - - /** - * Create a game session token for a player - * @param gameId - The database ID of the game - * @param gameCode - The public game code (e.g., ABC123) - * @param playerName - The player's name in the game - * @param userId - Optional user ID for authenticated players - * @returns Game session JWT token - */ - createGameToken(gameId: string, gameCode: string, playerName: string, userId?: string): string { - const now = Math.floor(Date.now() / 1000); - - const payload: GameTokenPayload = { - gameId, - gameCode, - playerName, - isAuthenticated: !!userId, - userId, - tokenType: 'game_session', - iat: now, - exp: now + this.gameTokenExpiry - }; - - const options: SignOptions = {}; - const token = jwt.sign(payload, this.secretKey, options); - - return token; - } - - /** - * Verify and decode a game session token - * @param token - The game session JWT token - * @returns Decoded payload or null if invalid - */ - verifyGameToken(token: string): GameTokenPayload | null { - try { - const decoded = jwt.verify(token, this.secretKey) as GameTokenPayload; - - // Verify it's actually a game token - if (decoded.tokenType !== 'game_session') { - return null; - } - - return decoded; - } catch (error) { - return null; - } - } - - /** - * Extract game token from request headers or query params - * @param req - Express request object - * @returns Game token string or null - */ - extractGameTokenFromRequest(req: Request): string | null { - // Check Authorization header - const authHeader = req.headers.authorization; - if (authHeader && authHeader.startsWith('Bearer ')) { - return authHeader.substring(7); - } - - // Check query parameter (for WebSocket handshake) - if (req.query && req.query.gameToken && typeof req.query.gameToken === 'string') { - return req.query.gameToken; - } - - // Check game_token cookie - if (req.cookies && req.cookies.game_token) { - return req.cookies.game_token; - } - - return null; - } - - /** - * Verify game token from request and return payload - * @param req - Express request object - * @returns Decoded game token payload or null - */ - verifyGameTokenFromRequest(req: Request): GameTokenPayload | null { - const token = this.extractGameTokenFromRequest(req); - if (!token) { - return null; - } - - return this.verifyGameToken(token); - } - - /** - * Check if a game token is valid for a specific game - * @param token - The game session token - * @param gameCode - The game code to validate against - * @param playerName - Optional player name to validate - * @returns True if token is valid for the game - */ - isValidForGame(token: string, gameCode: string, playerName?: string): boolean { - const payload = this.verifyGameToken(token); - if (!payload) { - return false; - } - - // Check game code matches - if (payload.gameCode !== gameCode) { - return false; - } - - // Check player name if provided - if (playerName && payload.playerName !== playerName) { - return false; - } - - return true; - } - - /** - * Refresh a game token (extend expiry) - * @param currentToken - The current game token - * @returns New token with extended expiry or null if invalid - */ - refreshGameToken(currentToken: string): string | null { - const payload = this.verifyGameToken(currentToken); - if (!payload) { - return null; - } - - // Create new token with same data but fresh expiry - return this.createGameToken( - payload.gameId, - payload.gameCode, - payload.playerName, - payload.userId - ); - } - - /** - * Get remaining time before token expires - * @param token - The game session token - * @returns Seconds until expiry or -1 if invalid/expired - */ - getTimeUntilExpiry(token: string): number { - const payload = this.verifyGameToken(token); - if (!payload || !payload.exp) { - return -1; - } - - const now = Math.floor(Date.now() / 1000); - const remaining = payload.exp - now; - - return remaining > 0 ? remaining : -1; - } - - /** - * Create a game token response object for API responses - * @param gameId - The database ID of the game - * @param gameCode - The public game code - * @param playerName - The player's name - * @param userId - Optional user ID for authenticated players - * @returns Object with token and metadata - */ - createGameTokenResponse(gameId: string, gameCode: string, playerName: string, userId?: string) { - const token = this.createGameToken(gameId, gameCode, playerName, userId); - const expiresIn = this.gameTokenExpiry; - - return { - gameToken: token, - gameCode, - playerName, - isAuthenticated: !!userId, - expiresIn, - expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(), - tokenType: 'game_session' - }; - } -} - -export default GameTokenService; \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts b/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts deleted file mode 100644 index fd2fcf14..00000000 --- a/SerpentRace_Backend/src/Application/Services/GameWebSocketService.ts +++ /dev/null @@ -1,3566 +0,0 @@ -import { Server as SocketIOServer, Socket } from 'socket.io'; -import { GameTokenService, GameTokenPayload } from './GameTokenService'; -import { GameRepository } from '../../Infrastructure/Repository/GameRepository'; -import { UserRepository } from '../../Infrastructure/Repository/UserRepository'; -import { TurnHistoryRepository } from '../../Infrastructure/Repository/TurnHistoryRepository'; -import { GameSnapshotRepository } from '../../Infrastructure/Repository/GameSnapshotRepository'; -import { GameAggregate, GameState, LoginType, GameField } from '../../Domain/Game/GameAggregate'; -import { logAuth, logError, logOther, logWarning } from './Logger'; -import { RedisService } from './RedisService'; -import { FieldEffectService, CardProcessingResult } from './FieldEffectService'; -import { CardDrawingService } from './CardDrawingService'; -import { BoardGenerationService } from '../Game/BoardGenerationService'; -import { GamemasterService, GamemasterDecision } from './GamemasterService'; -import { TurnHistoryService } from './TurnHistoryService'; -import { GameSnapshotService } from './GameSnapshotService'; -import { TurnActionType } from '../../Domain/Game/TurnHistoryAggregate'; -import { SnapshotTrigger } from '../../Domain/Game/GameSnapshotAggregate'; -import { - GameActionData, - PlayerPosition, - GameStateUpdateData, - FieldEffectRequest, - JoinGameData, - LeaveGameData -} from './Interfaces/GameInterfaces'; -import { json } from 'stream/consumers'; - -interface AuthenticatedSocket extends Socket { - userId?: string; - gameCode?: string; - playerName?: string; - isAuthenticated?: boolean; -} - -interface DiceRollData { - gameCode: string; - diceValue: number; // Value from frontend (1-6) -} - -interface GameChatData { - gameCode: string; - message: string; -} - -interface CardAnswerData { - gameCode: string; - answer: any; - cardId?: string; // Optional card ID sent from frontend -} - -interface GamemasterDecisionData { - gameCode: string; - requestId: string; - decision: 'approve' | 'reject'; -} - -interface PendingCardState { - playerId: string; - playerName: string; - card: any; - field: GameField; // Field info - dice: number; // Dice roll - currentPosition: number; // Position before card - landedPosition: number; // Position after dice roll (where they landed) - drawnAt: number; - answerGiven?: boolean; // Track if answer submitted - answerCorrect?: boolean; // Track if answer was correct - requiresGuess?: boolean; // Track if guess is required - guessedPosition?: number; // Store player's guess -} - -interface PendingDecisionState { - playerId: string; - playerName: string; - card: any; - field: GameField; // Field info - dice: number; // Dice roll (always 6 for jokers) - currentPosition: number; // Position before card - drawnAt: number; - recursionDepth: number; - gamemasterDecided?: boolean; // Track if gamemaster decided - gamemasterApproved?: boolean; // Track approval result - guessedPosition?: number; // Store player's guess -} - -export class GameWebSocketService { - private io: SocketIOServer; - private gameTokenService: GameTokenService; - private gameRepository: GameRepository; - private userRepository: UserRepository; - private redisService: RedisService; - private fieldEffectService: FieldEffectService; - private cardDrawingService: CardDrawingService; - private boardGenerationService: BoardGenerationService; - private gamemasterService: GamemasterService; - private turnHistoryService: TurnHistoryService; - private gameSnapshotService: GameSnapshotService; - - constructor( - io: SocketIOServer, - gameRepository: GameRepository, - userRepository: UserRepository, - redisService: RedisService, - turnHistoryRepository: TurnHistoryRepository, - gameSnapshotRepository: GameSnapshotRepository - ) { - this.io = io; - this.gameTokenService = new GameTokenService(); - this.gameRepository = gameRepository; - this.userRepository = userRepository; - this.redisService = redisService; - - // Initialize services in proper dependency order - this.boardGenerationService = new BoardGenerationService(); - this.gamemasterService = new GamemasterService(); - this.cardDrawingService = new CardDrawingService(); - this.fieldEffectService = new FieldEffectService( - this.boardGenerationService, - this.gamemasterService - ); - this.turnHistoryService = new TurnHistoryService(turnHistoryRepository); - this.gameSnapshotService = new GameSnapshotService(gameSnapshotRepository, redisService); - - this.setupGameNamespace(); - } - - private setupGameNamespace(): void { - // Create a namespace specifically for game events - const gameNamespace = this.io.of('/game'); - - gameNamespace.on('connection', (socket: AuthenticatedSocket) => { - logOther(`New game socket connection: ${socket.id}`); - - // For game sockets, authentication is optional (public games) - // Players will authenticate when joining a specific game - this.setupGameEventHandlers(socket); - }); - } - - private setupGameEventHandlers(socket: AuthenticatedSocket): void { - // Join game room - socket.on('game:join', async (data: any) => { - await this.handleJoinGame(socket, data); - }); - - // Leave game room - socket.on('game:leave', async (data: LeaveGameData) => { - await this.handleLeaveGame(socket, data); - }); - - // Game actions (dice roll, move, etc.) - socket.on('game:action', async (data: GameActionData) => { - await this.handleGameAction(socket, data); - }); - - // Game chat within a specific game - socket.on('game:chat', async (data: GameChatData) => { - await this.handleGameChat(socket, data); - }); - - // Player ready status - socket.on('game:ready', async (data: { gameCode: string; ready: boolean }) => { - await this.handlePlayerReady(socket, data); - }); - - // Gamemaster approve player (private games only) - socket.on('game:approve-player', async (data: { gameCode: string; playerName: string }) => { - await this.handleApprovePlayer(socket, data); - }); - - // Gamemaster reject player (private games only) - socket.on('game:reject-player', async (data: { gameCode: string; playerName: string; reason?: string }) => { - await this.handleRejectPlayer(socket, data); - }); - - // Player joining after approval (private games) - socket.on('game:join-approved', async (data: JoinGameData) => { - await this.handleJoinApproved(socket, data); - }); - - // Dice roll from frontend - socket.on('game:dice-roll', async (data: DiceRollData) => { - await this.handleDiceRoll(socket, data); - }); - - // Card answer from player - socket.on('game:card-answer', async (data: CardAnswerData) => { - await this.handleCardAnswer(socket, data); - }); - - // Gamemaster decision on joker card - socket.on('game:gamemaster-decision', async (data: GamemasterDecisionData) => { - await this.handleGamemasterDecision(socket, data); - }); - - // Position guess (for question cards) - socket.on('game:position-guess', async (data: { gameCode: string; guessedPosition: number }) => { - await this.handlePositionGuess(socket, data); - }); - - // Joker position guess (for joker cards) - socket.on('game:joker-position-guess', async (data: { gameCode: string; guessedPosition: number }) => { - await this.handleJokerPositionGuess(socket, data); - }); - - // Disconnect handling - socket.on('disconnect', async () => { - await this.handleDisconnect(socket); - }); - } - - private async handleJoinGame(socket: AuthenticatedSocket, data: any): Promise { - try { - // Socket.IO automatically deserializes JSON - data is already an object - const gameToken = data?.gameToken; - - if (!gameToken) { - logError('Game join failed: No game token provided'); - socket.emit('game:error', { message: 'Game token is required' }); - return; - } - - // Verify the game token - const gameTokenPayload = this.gameTokenService.verifyGameToken(gameToken); - if (!gameTokenPayload) { - logError('Game join failed: Invalid game token'); - socket.emit('game:error', { message: 'Invalid or expired game token' }); - return; - } - - const { gameId, gameCode, playerName, isAuthenticated, userId } = gameTokenPayload; - - // Validate game still exists - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game || game.id !== gameId) { - logError(`Game join failed: Game not found - Code: ${gameCode}`); - socket.emit('game:error', { message: 'Game not found or token invalid' }); - return; - } - - // Check if player name is already in use by checking connected players - const connectedPlayers = await this.getConnectedPlayers(gameCode); - if (connectedPlayers.includes(playerName)) { - logOther(`Game join failed: Player name "${playerName}" already in use in game ${gameCode}`); - socket.emit('game:error', { message: `Player name "${playerName}" is already in use in this game` }); - return; - } - - // Set socket properties from game token - socket.gameCode = gameCode; - socket.playerName = playerName; - socket.isAuthenticated = isAuthenticated; - socket.userId = userId; - - // Check if this is a private game and player needs gamemaster approval - const isGamemaster = game.createdby === userId; - const needsApproval = game.logintype === LoginType.PRIVATE && !isGamemaster; - - logOther(`Player joining game: ${playerName}`); - logOther(` - userId: ${userId}`); - logOther(` - game.createdby: ${game.createdby}`); - logOther(` - isGamemaster: ${isGamemaster}`); - logOther(` - needsApproval: ${needsApproval}`); - - // Generate dynamic room names (needed for both approval and direct join) - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - if (needsApproval) { - // For private games, non-gamemaster players need approval - // Add to pending players list and notify gamemaster - await this.addToPendingPlayers(gameCode, playerName); - - // Send pending status to the requesting player - socket.emit('game:pending-approval', { - gameCode, - playerName, - message: 'Waiting for gamemaster approval to join the game', - timestamp: new Date().toISOString() - }); - - // Notify gamemaster about the pending player - socket.to(gameRoomName).emit('game:player-requesting-join', { - playerName: playerName, - isAuthenticated, - message: `${playerName} is requesting to join the game`, - timestamp: new Date().toISOString() - }); - - return; // Don't join rooms yet - wait for approval - } - - // Join both the general game room and player-specific room - await socket.join(gameRoomName); - await socket.join(playerRoomName); - - // Update Redis with active player connection FIRST (before getting state) - await this.updatePlayerConnection(gameCode, playerName, true); - - // Send success response to the joining player - socket.emit('game:joined', { - gameCode, - playerName, - isAuthenticated, - gameId, - isGamemaster, - timestamp: new Date().toISOString() - }); - - - // Notify other players in the game (broadcast) - socket.to(gameRoomName).emit('game:player-joined', { - playerName: playerName, - isAuthenticated, - isGamemaster, - timestamp: new Date().toISOString() - }); - - - // Send current game state to the joining player (now includes this player) - const gameState = await this.getGameState(gameCode); - // Add isGamemaster flag for this specific player - const gameStateWithMasterFlag = { ...gameState, isGamemaster }; - socket.emit('game:state', gameStateWithMasterFlag); - - // Broadcast updated game state to all other players so they see the new player - socket.to(gameRoomName).emit('game:state-update', gameState); - - } catch (error) { - socket.emit('game:error', { - message: 'Failed to join game', - error: error instanceof Error ? error.message : 'Unknown error' - }); - } - } - - private async handleLeaveGame(socket: AuthenticatedSocket, data: LeaveGameData): Promise { - try { - const { gameCode } = data; - const playerName = socket.playerName; - - // Validate we have the required data - if (!playerName) { - logError('Cannot leave game: socket has no playerName'); - socket.emit('game:error', { message: 'Player has no name' }); - return; - } - - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - // Leave both rooms - await socket.leave(gameRoomName); - await socket.leave(playerRoomName); - - logOther(`Player ${playerName} left game room: ${gameRoomName}`); - - // Notify other players - socket.to(gameRoomName).emit('game:player-left', { - playerName: playerName, - timestamp: new Date().toISOString() - }); - - // Update Redis before clearing socket properties - await this.updatePlayerConnection(gameCode, playerName, false); - - // Clear socket properties - socket.gameCode = undefined; - socket.playerName = undefined; - - } catch (error) { - logError('Error leaving game', error as Error); - socket.emit('game:error', { message: 'Failed to leave game' }); - } - } - - private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData): Promise { - try { - const { gameCode, action, data: actionData } = data; - - if (!socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to perform actions' }); - return; - } - - // Validate it's the player's turn (this would need game state logic) - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game) { - socket.emit('game:error', { message: 'Game not found' }); - return; - } - - // Process the game action based on type - const result = await this.processGameAction(game, socket.userId!, action, actionData); - - if (result.success) { - // Broadcast action to all players in the game - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:action-result', { - action, - playerName: socket.playerName, - result: result.data, - timestamp: new Date().toISOString() - }); // If the action resulted in a game state change, broadcast the new state - if (result.stateChanged) { - const updatedGameState = await this.getGameState(gameCode); - this.io.of('/game').to(gameRoomName).emit('game:state-update', updatedGameState); - } - } else { - socket.emit('game:error', { message: result.error }); - } - - } catch (error) { - logError('Error processing game action', error as Error); - socket.emit('game:error', { message: 'Failed to process action' }); - } - } - - private async handleGameChat(socket: AuthenticatedSocket, data: GameChatData): Promise { - try { - const { gameCode, message } = data; - - if (!socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to chat' }); - return; - } - - const gameRoomName = `game_${gameCode}`; - - // Broadcast chat message to all players in the game - this.io.of('/game').to(gameRoomName).emit('game:chat-message', { - playerName: socket.playerName, - message, - timestamp: new Date().toISOString() - }); - - logOther(`Game chat in ${gameCode}: ${socket.playerName || socket.userId}: ${message}`); - - } catch (error) { - logError('Error handling game chat', error as Error); - socket.emit('game:error', { message: 'Failed to send chat message' }); - } - } - - private async handlePlayerReady(socket: AuthenticatedSocket, data: { gameCode: string; ready: boolean }): Promise { - try { - const { gameCode, ready } = data; - const gameRoomName = `game_${gameCode}`; - - // Update player ready status in Redis - await this.updatePlayerReadyStatus(gameCode, socket.playerName!, ready); - - // Broadcast ready status to all players - this.io.of('/game').to(gameRoomName).emit('game:player-ready', { - playerName: socket.playerName, - ready, - timestamp: new Date().toISOString() - }); - - // Check if all players are ready and start game if so - const allReady = await this.checkAllPlayersReady(gameCode); - if (allReady) { - this.io.of('/game').to(gameRoomName).emit('game:all-ready', { - message: 'All players are ready! Game can start.', - timestamp: new Date().toISOString() - }); - } - - } catch (error) { - logError('Error handling player ready status', error as Error); - socket.emit('game:error', { message: 'Failed to update ready status' }); - } - } - - private async handleApprovePlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string }): Promise { - try { - const { gameCode, playerName } = data; - - // Verify that the requesting socket is the gamemaster - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game) { - socket.emit('game:error', { message: 'Game not found' }); - return; - } - - const isGamemaster = game.createdby === socket.userId; - if (!isGamemaster) { - socket.emit('game:error', { message: 'Only the gamemaster can approve players' }); - return; - } - - if (game.logintype !== LoginType.PRIVATE) { - socket.emit('game:error', { message: 'Player approval is only for private games' }); - return; - } - - // Check if player is in pending list - const pendingPlayers = await this.getPendingPlayers(gameCode); - if (!pendingPlayers.includes(playerName)) { - socket.emit('game:error', { message: 'Player not found in pending list' }); - return; - } - - // Remove from pending players - await this.removeFromPendingPlayers(gameCode, playerName); - - // Notify the approved player to join the game rooms - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - // Find the pending player's socket and move them to the game - this.io.of('/game').emit('game:approval-granted', { - gameCode, - playerName, - gameRoomName, - playerRoomName, - message: 'You have been approved to join the game!', - timestamp: new Date().toISOString() - }); - - // Notify all players about the approval - this.io.of('/game').to(gameRoomName).emit('game:player-approved', { - playerName, - approvedBy: socket.playerName, - timestamp: new Date().toISOString() - }); - - // Send updated game state to gamemaster to preserve their status - const gameState = await this.getGameState(gameCode); - const gamemasterState = { ...gameState, isGamemaster: true }; - socket.emit('game:state', gamemasterState); - - logOther(`Player ${playerName} approved by gamemaster in game ${gameCode}`); - - } catch (error) { - logError('Error approving player', error as Error); - socket.emit('game:error', { message: 'Failed to approve player' }); - } - } - - private async handleRejectPlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string; reason?: string }): Promise { - try { - const { gameCode, playerName, reason } = data; - - // Verify that the requesting socket is the gamemaster - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game) { - socket.emit('game:error', { message: 'Game not found' }); - return; - } - - const isGamemaster = game.createdby === socket.userId; - if (!isGamemaster) { - socket.emit('game:error', { message: 'Only the gamemaster can reject players' }); - return; - } - - if (game.logintype !== LoginType.PRIVATE) { - socket.emit('game:error', { message: 'Player rejection is only for private games' }); - return; - } - - // Check if player is in pending list - const pendingPlayers = await this.getPendingPlayers(gameCode); - if (!pendingPlayers.includes(playerName)) { - socket.emit('game:error', { message: 'Player not found in pending list' }); - return; - } - - // Remove from pending players - await this.removeFromPendingPlayers(gameCode, playerName); - - // Notify the rejected player - this.io.of('/game').emit('game:approval-denied', { - gameCode, - playerName, - reason: reason || 'Your request to join the game was denied', - timestamp: new Date().toISOString() - }); - - // Send updated game state to gamemaster to preserve their status - const gameState = await this.getGameState(gameCode); - const gamemasterState = { ...gameState, isGamemaster: true }; - socket.emit('game:state', gamemasterState); - - logOther(`Player ${playerName} rejected by gamemaster in game ${gameCode}${reason ? ': ' + reason : ''}`); - - } catch (error) { - logError('Error rejecting player', error as Error); - socket.emit('game:error', { message: 'Failed to reject player' }); - } - } - - private async handleJoinApproved(socket: AuthenticatedSocket, data: JoinGameData): Promise { - try { - const { gameToken } = data; - - if (!gameToken) { - socket.emit('game:error', { message: 'Game token is required' }); - return; - } - - // Verify the game token - const gameTokenPayload = this.gameTokenService.verifyGameToken(gameToken); - if (!gameTokenPayload) { - socket.emit('game:error', { message: 'Invalid or expired game token' }); - return; - } - - const { gameId, gameCode, playerName, isAuthenticated, userId } = gameTokenPayload; - - // Validate game still exists - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game || game.id !== gameId) { - socket.emit('game:error', { message: 'Game not found or token invalid' }); - return; - } - - // Check if player was actually approved (not in pending list anymore) - const pendingPlayers = await this.getPendingPlayers(gameCode); - if (pendingPlayers.includes(playerName)) { - socket.emit('game:error', { message: 'Player still pending approval' }); - return; - } - - // Set socket properties from game token - socket.gameCode = gameCode; - socket.playerName = playerName; - socket.isAuthenticated = isAuthenticated; - socket.userId = userId; - - // Generate dynamic room names and join - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - await socket.join(gameRoomName); - await socket.join(playerRoomName); - - logOther(`Approved player ${playerName} joined game room: ${gameRoomName}`); - - // Update Redis with active player connection FIRST (before getting state) - await this.updatePlayerConnection(gameCode, playerName, true); - - // Send success response to the joining player - socket.emit('game:joined', { - gameCode, - playerName, - isAuthenticated, - gameId, - isGamemaster: false, - timestamp: new Date().toISOString() - }); - - // Notify other players in the game (broadcast) - socket.to(gameRoomName).emit('game:player-joined', { - playerName: playerName, - isAuthenticated, - isGamemaster: false, - timestamp: new Date().toISOString() - }); - - // Send current game state to the joining player (after approval) - const gameState = await this.getGameState(gameCode); - // Check if this player is gamemaster (shouldn't be, since they were just approved) - const gameForMasterCheck = await this.gameRepository.findByGameCode(gameCode); - const playerIsGamemaster = gameForMasterCheck?.createdby === socket.userId; - const gameStateWithMasterFlag = { ...gameState, isGamemaster: playerIsGamemaster }; - socket.emit('game:state', gameStateWithMasterFlag); - - // Broadcast updated game state to all other players so they see the new player - socket.to(gameRoomName).emit('game:state-update', gameState); - - } catch (error) { - logError('Error handling approved join', error as Error); - socket.emit('game:error', { message: 'Failed to join after approval' }); - } - } - - private async handleDiceRoll(socket: AuthenticatedSocket, data: DiceRollData): Promise { - try { - const { gameCode, diceValue } = data; - - // Validate input - if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to roll dice' }); - return; - } - - if (!diceValue || diceValue < 1 || diceValue > 6) { - socket.emit('game:error', { message: 'Invalid dice value. Must be between 1 and 6' }); - return; - } - - // Get current game state - const gameState = await this.getCurrentGameState(gameCode); - if (!gameState) { - socket.emit('game:error', { message: 'Game not found' }); - return; - } - - // Check if it's the player's turn - // Use userId for authenticated players, playerName for guests - const playerIdentifier = socket.userId || socket.playerName; - if (!playerIdentifier) { - socket.emit('game:error', { message: 'Player identification failed' }); - return; - } - - if (gameState.currentPlayer !== playerIdentifier) { - socket.emit('game:error', { message: 'It is not your turn' }); - return; - } - - // Get player's current position - const playerPositions = await this.getPlayerPositions(gameCode); - const currentPlayer = playerPositions.find(p => p.playerId === playerIdentifier); - - if (!currentPlayer) { - socket.emit('game:error', { message: 'Player not found in game' }); - return; - } - - // Calculate new position after dice roll - let newPosition = Math.min(currentPlayer.boardPosition + diceValue, 100); // Win at 100 - - const gameRoomName = `game_${gameCode}`; - - // Emit dice-rolled event FIRST (for frontend animation) - this.io.of('/game').to(gameRoomName).emit('game:dice-rolled', { - playerId: playerIdentifier, - playerName: socket.playerName, - diceValue: diceValue, - calculatedDestination: newPosition, - timestamp: new Date().toISOString() - }); - - // Emit player-moving event (token starts animation) - this.io.of('/game').to(gameRoomName).emit('game:player-moving', { - playerId: playerIdentifier, - playerName: socket.playerName, - fromPosition: currentPlayer.boardPosition, - toPosition: newPosition, - timestamp: new Date().toISOString() - }); - - // Check if player won (reached position 100) - if (newPosition >= 100) { - // Update position BEFORE ending game - await this.updatePlayerPosition(gameCode, playerIdentifier, newPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: playerIdentifier, - playerName: socket.playerName, - position: newPosition, - fieldType: 'finish', - timestamp: new Date().toISOString() - }); - - await this.endGame(gameCode, playerIdentifier, socket.playerName!); - return; - } - - // Check if player landed on special field (positive, negative, or luck) - const boardData = await this.getBoardData(gameCode); - if (boardData && boardData.fields) { - const landedField = boardData.fields.find((f: GameField) => f.position === newPosition); - - if (landedField && this.isSpecialField(landedField)) { - // Wait 2 seconds for frontend animation to complete - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Process special field - draw card - // Position will be updated AFTER card/consequence logic in handleSpecialFieldLanding - await this.handleSpecialFieldLanding( - gameCode, - playerIdentifier, - socket.playerName!, - landedField, - newPosition, - diceValue, - currentPlayer.boardPosition - ); - return; // Don't advance turn yet, waiting for card answer - } - } - - // No special field - update position now and advance turn - await this.updatePlayerPosition(gameCode, playerIdentifier, newPosition); - - // Log turn history - await this.turnHistoryService.logTurnAction( - gameCode, - playerIdentifier, - socket.playerName!, - gameState.turnNumber || 1, - TurnActionType.DICE_ROLL, - currentPlayer.boardPosition, - newPosition, - { diceValue } - ); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: playerIdentifier, - playerName: socket.playerName, - position: newPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - await this.advanceTurn(gameCode); - - logOther(`Player ${socket.playerName} rolled ${diceValue}, moved from ${currentPlayer.boardPosition} to ${newPosition}`, { - gameCode, - playerId: socket.userId - }); - - } catch (error) { - logError('Error handling dice roll', error as Error); - socket.emit('game:error', { message: 'Failed to process dice roll' }); - } - } - - private async handleCardAnswer(socket: AuthenticatedSocket, data: CardAnswerData): Promise { - try { - const { gameCode, answer, cardId } = data; - - logOther(`Card answer received`, { - gameCode, - playerId: socket.userId, - playerName: socket.playerName, - cardId, - answerType: typeof answer - }); - - // Validate input - if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to answer cards' }); - return; - } - - if (!socket.userId) { - socket.emit('game:error', { message: 'Player not authenticated' }); - return; - } - - // Get pending card from Redis - const pendingCard = await this.getPendingCard(gameCode, socket.userId); - if (!pendingCard) { - logError(`No pending card found for player ${socket.playerName} (${socket.userId}) in game ${gameCode}`); - socket.emit('game:error', { message: 'No pending card answer found' }); - return; - } - - const pendingState = pendingCard as PendingCardState; - - // Clear the timeout by clearing from CardDrawingService - const answerKey = `${gameCode}:${socket.userId}`; - this.cardDrawingService.clearAnswerTimeout(answerKey); - - const gameRoomName = `game_${gameCode}`; - - // Broadcast player's answer to all players BEFORE validation - this.io.of('/game').to(gameRoomName).emit('game:answer-submitted', { - playerName: socket.playerName, - playerId: socket.userId, - answer: answer, - message: `${socket.playerName} answered: ${JSON.stringify(answer)}`, - timestamp: new Date().toISOString() - }); - - // Add dramatic pause before showing result - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Process the answer - const result = this.cardDrawingService.processAnswer(pendingState.card, answer); - - // Update pending state - pendingState.answerGiven = true; - pendingState.answerCorrect = result.correct; - - // Broadcast validation result - this.io.of('/game').to(gameRoomName).emit('game:answer-validated', { - playerName: socket.playerName, - playerId: socket.userId, - isCorrect: result.correct, - correctAnswer: pendingState.card.answer, - message: result.correct - ? `✅ ${socket.playerName} answered correctly!` - : `❌ ${socket.playerName} answered incorrectly. Correct answer: ${JSON.stringify(pendingState.card.answer)}`, - timestamp: new Date().toISOString() - }); - - // Log answer submission - const gameState = await this.getCurrentGameState(gameCode); - if (gameState) { - await this.turnHistoryService.logTurnAction( - gameCode, - socket.userId!, - socket.playerName!, - gameState.turnNumber || 1, - TurnActionType.ANSWER_SUBMITTED, - pendingState.currentPosition, - pendingState.currentPosition, - { - cardId: pendingState.card.cardid, - answer: answer, - isCorrect: result.correct - } - ); - } - - // ========================================== - // NEW: Determine if position guess is required - // ========================================== - const requiresGuess = this.determineGuessRequirement( - pendingState.field.type, - result.correct - ); - - if (requiresGuess) { - // Request position guess - try { - await this.requestPositionGuess(gameCode, socket.userId, socket.playerName!, pendingState); - } catch (error) { - logError('Error requesting position guess, advancing turn', error as Error); - await this.clearPendingCard(gameCode, socket.userId); - await this.advanceTurn(gameCode); - } - } else { - // No guess required, handle based on field type - if (pendingState.field.type === 'positive' && !result.correct) { - // Positive field + wrong answer = stay at landed position (no movement back) - this.io.of('/game').to(gameRoomName).emit('game:no-movement', { - playerId: socket.userId, - playerName: socket.playerName, - reason: 'Wrong answer on positive field', - message: `${socket.playerName} stays at position ${pendingState.landedPosition}`, - timestamp: new Date().toISOString() - }); - } else if (pendingState.field.type === 'negative' && result.correct) { - // Negative field + correct answer = stay at landed position (avoided penalty) - this.io.of('/game').to(gameRoomName).emit('game:penalty-avoided', { - playerId: socket.userId, - playerName: socket.playerName, - message: `${socket.playerName} avoided the penalty! Stays at position ${pendingState.landedPosition}`, - timestamp: new Date().toISOString() - }); - } - - // Emit player-arrived event (stay at landed position, not reverting to pre-dice position) - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: socket.userId, - playerName: socket.playerName, - position: pendingState.landedPosition || pendingState.currentPosition, // Use landed position - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - // Clean up and advance turn - await this.clearPendingCard(gameCode, socket.userId); - await this.advanceTurn(gameCode); - } - - logOther(`Player ${socket.playerName} answered card: ${result.correct ? 'correct' : 'wrong'}`, { - gameCode, - playerId: socket.userId, - requiresGuess - }); - - } catch (error) { - logError('Error handling card answer', error as Error); - socket.emit('game:error', { message: 'Failed to process card answer' }); - // Ensure turn advances even on error - try { - if (socket.userId && data.gameCode) { - const pendingCard = await this.getPendingCard(data.gameCode, socket.userId); - if (pendingCard) { - await this.clearPendingCard(data.gameCode, socket.userId); - } - await this.advanceTurn(data.gameCode); - } - } catch (advanceError) { - logError('Error advancing turn after card answer error', advanceError as Error); - } - } - } - - private async handleGamemasterDecision(socket: AuthenticatedSocket, data: GamemasterDecisionData): Promise { - try { - const { gameCode, requestId, decision } = data; - - // Validate input - if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { - socket.emit('game:error', { message: 'You must be in the game to make decisions' }); - return; - } - - if (!socket.userId) { - socket.emit('game:error', { message: 'Gamemaster not authenticated' }); - return; - } - - // Verify this is the gamemaster - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game || game.createdby !== socket.userId) { - socket.emit('game:error', { message: 'Only the gamemaster can make this decision' }); - return; - } - - // Get pending decision from Redis - const pendingDecision = await this.getPendingDecision(gameCode, requestId); - if (!pendingDecision) { - socket.emit('game:error', { message: 'Decision request not found or expired' }); - return; - } - - const pendingState = pendingDecision as PendingDecisionState; - - // Process decision through GamemasterService - const result = this.gamemasterService.processGamemasterDecision( - requestId, - decision === 'approve' ? GamemasterDecision.APPROVE : GamemasterDecision.REJECT - ); - - if (!result) { - socket.emit('game:error', { message: 'Failed to process gamemaster decision' }); - // Clean up - await this.clearPendingDecision(gameCode, requestId); - return; - } - - const gameRoomName = `game_${gameCode}`; - const approved = decision === 'approve'; - - // Update pending state with decision - pendingState.gamemasterDecided = true; - pendingState.gamemasterApproved = approved; - - // Broadcast decision result to all players - this.io.of('/game').to(gameRoomName).emit('game:gamemaster-decision-result', { - playerName: pendingState.playerName, - playerId: pendingState.playerId, - gamemasterName: socket.playerName, - decision: decision, - approved: approved, - consequence: result.consequence, - description: result.description, - timestamp: new Date().toISOString() - }); - - // ========================================== - // NEW: Determine if position guess is required - // ========================================== - const requiresGuess = ( - (pendingState.field.type === 'positive' && approved) || - (pendingState.field.type === 'negative' && !approved) - ); - - if (requiresGuess) { - // Request position guess - await this.requestJokerPositionGuess( - gameCode, - pendingState.playerId, - pendingState.playerName, - pendingState - ); - } else { - // No guess required - if (pendingState.field.type === 'positive' && !approved) { - // Positive field + rejected = no movement (stay at currentPosition) - this.io.of('/game').to(gameRoomName).emit('game:no-movement', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - reason: 'Gamemaster rejected on positive field', - message: `${pendingState.playerName} stays at position ${pendingState.currentPosition}`, - timestamp: new Date().toISOString() - }); - } else if (pendingState.field.type === 'negative' && approved) { - // Negative field + approved = no movement (avoided penalty, stay at currentPosition) - this.io.of('/game').to(gameRoomName).emit('game:penalty-avoided', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - message: `${pendingState.playerName} avoided the penalty! Stays at position ${pendingState.currentPosition}`, - timestamp: new Date().toISOString() - }); - } - - // Emit player-arrived event (stayed at original position) - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - position: pendingState.currentPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - // Clean up and advance turn - await this.clearPendingDecision(gameCode, requestId); - await this.advanceTurn(gameCode); - } - - logOther(`Gamemaster ${socket.playerName} made decision: ${decision} for player ${pendingState.playerName}`, { - gameCode, - requestId, - requiresGuess - }); - - } catch (error) { - logError('Error handling gamemaster decision', error as Error); - socket.emit('game:error', { message: 'Failed to process gamemaster decision' }); - } - } - - /** - * Handle player landing on special field (positive, negative, luck) - * Draws card and initiates appropriate flow based on card type - */ - private async handleSpecialFieldLanding( - gameCode: string, - playerId: string, - playerName: string, - field: GameField, - position: number, - dice: number, - currentPosition: number - ): Promise { - try { - const gameRoomName = `game_${gameCode}`; - - // Get game data for card drawing - const gameData = await this.gameRepository.findByGameCode(gameCode); - if (!gameData) { - logError('Game not found when handling special field landing'); - await this.advanceTurn(gameCode); - return; - } - - // Draw a card based on field type - const cardDrawResult = this.cardDrawingService.drawCard( - gameData, - field.type as 'positive' | 'negative' | 'luck', - playerId - ); - - if (!cardDrawResult.success || !cardDrawResult.card) { - // No more cards available or error - this.io.of('/game').to(gameRoomName).emit('game:card-error', { - playerName, - playerId, - error: cardDrawResult.error || 'Failed to draw card', - timestamp: new Date().toISOString() - }); - await this.advanceTurn(gameCode); - return; - } - - const card = cardDrawResult.card; - - // Check if card has consequence (joker/luck card) even without type field - const hasConsequence = card.consequence !== undefined && card.consequence !== null; - const isLuckType = this.isLuckCard(card.type); - - // Get game state for turn number - const gameState = await this.getCurrentGameState(gameCode); - - // Broadcast card drawn to all players (everyone sees the question) - this.io.of('/game').to(gameRoomName).emit('game:card-drawn', { - playerName, - playerId, - cardType: hasConsequence ? 'joker' : this.getCardTypeName(card.type), - question: card.question, - fieldType: field.type, - timestamp: new Date().toISOString() - }); - - logOther('Card drawn event broadcasted', { - playerName, - playerId, - cardType: hasConsequence ? 'joker' : this.getCardTypeName(card.type), - hasConsequence, - cardTypeNumber: card.type, - question: card.question?.substring(0, 50), - fieldType: field.type - }); - - // Log card drawn - if (gameState) { - await this.turnHistoryService.logTurnAction( - gameCode, - playerId, - playerName, - gameState.turnNumber || 1, - TurnActionType.CARD_DRAWN, - currentPosition, - currentPosition, // Position unchanged at this point - { - cardId: card.cardid, - cardType: hasConsequence ? 'joker' : this.getCardTypeName(card.type), - question: card.question, - fieldType: field.type - } - ); - } - - // Check card type and handle accordingly - if (isLuckType || hasConsequence) { - // Update position to destination FIRST (player has landed on the luck field) - await this.updatePlayerPosition(gameCode, playerId, position); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: position, - fieldType: field.type, - timestamp: new Date().toISOString() - }); - - // Luck card - process immediately (no answer required) - const result = this.cardDrawingService.processLuckCard(card); - - // Broadcast luck result - this.io.of('/game').to(gameRoomName).emit('game:card-result', { - playerName, - playerId, - correct: true, - consequence: result.consequence, - description: result.description, - timestamp: new Date().toISOString() - }); - - // Process luck card with multi-turn support - // Note: processLuckCard will update position again if consequence moves player - if (card.consequence) { - await this.processLuckCard(gameCode, playerId, playerName, card.consequence, position); - } else { - // Fallback to old method if no consequence object - await this.applyCardConsequence(gameCode, playerId, playerName, result.consequence); - } - } else { - // Question card - send to player for answer - if (!cardDrawResult.clientData) { - logError('Client data missing for question card'); - logOther('Card details for missing clientData', { - cardType: card.type, - cardId: card.cardid, - hasCard: !!card, - cardKeys: card ? Object.keys(card) : [] - }); - await this.advanceTurn(gameCode); - return; - } - - // Update position to destination FIRST (player has landed on the field) - await this.updatePlayerPosition(gameCode, playerId, position); - - // Emit player-arrived event so frontend shows the position - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: position, - fieldType: field.type, - timestamp: new Date().toISOString() - }); - - // Send interactive card to the player who drew it - const playerRoomName = `game_${gameCode}:${playerName}`; - this.io.of('/game').to(playerRoomName).emit('game:card-drawn-self', { - cardData: cardDrawResult.clientData, - timeLimit: 60, // 60 seconds to answer - timestamp: new Date().toISOString() - }); - - logOther('Card sent to player for answer', { - playerName, - playerId, - cardId: card.cardid, - cardType: this.getCardTypeName(card.type), - hasClientData: !!cardDrawResult.clientData, - clientDataKeys: cardDrawResult.clientData ? Object.keys(cardDrawResult.clientData) : [] - }); - - // Start answer timeout - const answerKey = this.cardDrawingService.startAnswerTimeout( - gameCode, - playerId, - card, - this.handleCardAnswerTimeout.bind(this) - ); - - // Store pending card in Redis - await this.storePendingCard(gameCode, playerId, { - playerId: playerId, - playerName: playerName, - card: card, - field: field, - dice: dice, - currentPosition: currentPosition, - landedPosition: position, // Store where they landed - drawnAt: Date.now() - }); - - logOther(`Stored pending card for player ${playerName}`, { - gameCode, - playerId, - cardType: this.getCardTypeName(card.type), - cardId: card.cardid, - redisKey: `game_pending_card:${gameCode}:${playerId}` - }); - } - - } catch (error) { - logError('Error handling special field landing', error as Error); - await this.advanceTurn(gameCode); - } - } - - /** - * Handle card answer timeout (player didn't answer in time) - */ - private async handleCardAnswerTimeout(gameCode: string, playerId: string, card: any): Promise { - try { - // Clear from Redis - await this.clearPendingCard(gameCode, playerId); - - const gameRoomName = `game_${gameCode}`; - const pendingCard = await this.getPendingCard(gameCode, playerId); - const playerName = pendingCard?.playerName || 'Player'; - - // Broadcast timeout to all players - this.io.of('/game').to(gameRoomName).emit('game:card-timeout', { - playerName, - playerId, - message: '⏰ Time\'s up!', - timestamp: new Date().toISOString() - }); - - // Process as timeout (automatic wrong answer) - const result = this.cardDrawingService.processTimeoutAnswer(card); - - // Broadcast result - this.io.of('/game').to(gameRoomName).emit('game:card-result', { - playerName, - playerId, - correct: false, - consequence: result.consequence, - description: result.description, - timestamp: new Date().toISOString() - }); - - // Apply penalty - await this.applyCardConsequence(gameCode, playerId, playerName, result.consequence); - - } catch (error) { - logError('Error handling card answer timeout', error as Error); - } - } - - /** - * Get human-readable card type name - */ - private getCardTypeName(cardType?: number): string { - if (cardType === undefined) return 'unknown'; - - const typeNames = ['quiz', 'sentence_pairing', 'own_answer', 'true_false', 'closer', 'joker', 'luck']; - return typeNames[cardType] || 'unknown'; - } - - private async handleDisconnect(socket: AuthenticatedSocket): Promise { - logOther(`Game socket disconnected: ${socket.id} (player: ${socket.playerName})`); - - // If the socket was in a game, handle cleanup - if (socket.gameCode && socket.playerName) { - try { - // Check if this player is the gamemaster - const game = await this.gameRepository.findByGameCode(socket.gameCode); - const isGamemaster = game && socket.userId && game.createdby === socket.userId; - - // If gamemaster leaves, end the game immediately - if (isGamemaster && game) { - logOther(`Gamemaster ${socket.playerName} left game ${socket.gameCode}, ending game`); - - const gameRoomName = `game_${socket.gameCode}`; - - // Notify all players - this.io.of('/game').to(gameRoomName).emit('game:ended', { - reason: 'gamemaster_left', - gamemasterName: socket.playerName, - message: `🎭 Gamemaster ${socket.playerName} left. Game has ended.`, - timestamp: new Date().toISOString() - }); - - // Update database - await this.gameRepository.update(game.id, { - state: GameState.CANCELLED, - enddate: new Date() - }); - - // Clean up all game data - await this.cleanupGameData(socket.gameCode, game.id); - - return; // Exit early, no need for further cleanup - } - - // Clean up any pending card answer - if (socket.userId) { - const pendingCard = await this.getPendingCard(socket.gameCode, socket.userId); - if (pendingCard) { - // Clear timeout - const answerKey = `${socket.gameCode}:${socket.userId}`; - this.cardDrawingService.clearAnswerTimeout(answerKey); - await this.clearPendingCard(socket.gameCode, socket.userId); - - // Notify others - const gameRoomName = `game_${socket.gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:player-disconnected-during-card', { - playerName: socket.playerName, - playerId: socket.userId, - timestamp: new Date().toISOString() - }); - } - } - - // Update player connection status - await this.updatePlayerConnection(socket.gameCode, socket.playerName, false); - - // Create snapshot on player disconnect during active game - const gameState = await this.getCurrentGameState(socket.gameCode); - if (gameState) { - await this.gameSnapshotService.createSnapshot( - socket.gameCode, - (gameState.currentTurn || 0) + 1, - SnapshotTrigger.PLAYER_DISCONNECT, - `Player ${socket.playerName} disconnected` - ).catch(err => { - logError('Failed to create disconnect snapshot', err as Error); - logOther('Disconnect snapshot context', { - gameCode: socket.gameCode, - playerName: socket.playerName - }); - }); - } - - // Check if disconnected player was current player - const playerIdentifier = socket.userId || socket.playerName; - if (gameState && gameState.currentPlayer === playerIdentifier) { - logOther(`Current player ${socket.playerName} disconnected, advancing turn`, { gameCode: socket.gameCode }); - - // Broadcast disconnect during turn - const gameRoomName = `game_${socket.gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:player-disconnected-during-turn', { - playerName: socket.playerName, - playerId: playerIdentifier, - message: `${socket.playerName} disconnected during their turn`, - timestamp: new Date().toISOString() - }); - - // Advance to next player - await this.advanceTurn(socket.gameCode); - } - - // Clean up player-specific Redis data - await this.cleanupPlayerData(socket.gameCode, socket.playerName, socket.userId); - - // Notify other players about disconnection - const gameRoomName = `game_${socket.gameCode}`; - socket.to(gameRoomName).emit('game:player-disconnected', { - playerName: socket.playerName, - playerId: socket.userId, - timestamp: new Date().toISOString() - }); - - // Check if this was the last player - if so, consider ending/cleaning the game - const connectedPlayers = await this.getConnectedPlayers(socket.gameCode); - if (connectedPlayers.length === 0) { - logOther(`All players disconnected from game ${socket.gameCode}, scheduling cleanup`); - // Schedule cleanup after a delay to allow for reconnections - setTimeout(async () => { - const stillConnected = await this.getConnectedPlayers(socket.gameCode!); - if (stillConnected.length === 0) { - await this.handleAbandonedGame(socket.gameCode!); - } - }, 60000); // 1 minute delay - } - - } catch (error) { - logError('Error updating player connection on disconnect', error as Error); - logOther('Disconnect error context', { gameCode: socket.gameCode, playerName: socket.playerName }); - } - } - } - - /** - * Clean up player-specific data when they disconnect - * @param gameCode Game code - * @param playerName Player name - * @param playerId Player ID - */ - private async cleanupPlayerData(gameCode: string, playerName: string, playerId?: string): Promise { - try { - // Remove from ready players - await this.redisService.setRemove(`game_ready:${gameCode}`, playerName); - - // Remove from pending players if they were pending - await this.redisService.setRemove(`game_pending:${gameCode}`, playerName); - - logOther(`Cleaned up player data for ${playerName} in game ${gameCode}`); - - } catch (error) { - logError('Error cleaning up player data', error as Error); - } - } - - /** - * Handle games that have been abandoned by all players - * @param gameCode Game code - */ - private async handleAbandonedGame(gameCode: string): Promise { - try { - const game = await this.gameRepository.findByGameCode(gameCode); - if (!game) return; - - // Only clean up games that haven't finished yet - if (game.state !== GameState.FINISHED && game.state !== GameState.CANCELLED) { - logOther(`Handling abandoned game ${gameCode}`, { gameId: game.id }); - - // Mark game as cancelled in database - await this.gameRepository.update(game.id, { - state: GameState.CANCELLED, - enddate: new Date(), - }); - - // Clean up all Redis data for this abandoned game - await this.cleanupGameData(gameCode, game.id); - - logOther(`Abandoned game ${gameCode} has been cleaned up`); - } - - } catch (error) { - logError('Error handling abandoned game', error as Error); - } - } - - // Helper methods for game state management - - private async getGameState(gameCode: string): Promise { - try { - // Try gameplay first (game started/in-progress) - const gameplayState = await this.getCurrentGameState(gameCode); - if (gameplayState) { - return gameplayState; - } - - // Fallback to game: key for pre-game lobby - const gameKey = `game:${gameCode}`; - const gameStr = await this.redisService.get(gameKey); - if (gameStr) { - const gameData = JSON.parse(gameStr); - // Add pending players for private games - if (gameData.logintype === LoginType.PRIVATE) { - gameData.pendingPlayers = await this.getPendingPlayers(gameCode); - } - return gameData; - } - - return null; - } catch (error) { - logError('Error getting game state', error as Error); - return null; - } - } - - private async processGameAction(game: GameAggregate, playerId: string, action: string, actionData: any): Promise<{ success: boolean; data?: any; error?: string; stateChanged?: boolean }> { - // This would contain the actual game logic - // For now, returning a placeholder - - switch (action) { - case 'roll-dice': - // Handle dice rolling logic - const diceResult = Math.floor(Math.random() * 6) + 1; - return { - success: true, - data: { dice: diceResult }, - stateChanged: true - }; - - case 'move': - // Handle player movement logic - return { - success: true, - data: { newPosition: actionData.position }, - stateChanged: true - }; - - case 'use-field': - // Handle special field usage - return { - success: true, - data: { fieldUsed: actionData.fieldType }, - stateChanged: true - }; - - case 'end-turn': - // Handle turn ending logic - return { - success: true, - data: { nextPlayer: 'next-player-id' }, - stateChanged: true - }; - - default: - return { - success: false, - error: 'Unknown action type' - }; - } - } - - private async updatePlayerConnection(gameCode: string, playerName: string, connected: boolean): Promise { - const key = `game_connections:${gameCode}`; - if (connected) { - await this.redisService.setAdd(key, playerName); - } else { - await this.redisService.setRemove(key, playerName); - } - // Note: RedisService doesn't have expire method, we'll handle expiration differently - } - - private async updatePlayerReadyStatus(gameCode: string, playerName: string, ready: boolean): Promise { - const key = `game_ready:${gameCode}`; - if (ready) { - await this.redisService.setAdd(key, playerName); - } else { - await this.redisService.setRemove(key, playerName); - } - // Note: RedisService doesn't have expire method, we'll handle expiration differently - } - - private async addToPendingPlayers(gameCode: string, playerName: string): Promise { - const key = `game_pending:${gameCode}`; - await this.redisService.setAdd(key, playerName); - } - - private async removeFromPendingPlayers(gameCode: string, playerName: string): Promise { - const key = `game_pending:${gameCode}`; - await this.redisService.setRemove(key, playerName); - } - - private async getPendingPlayers(gameCode: string): Promise { - const key = `game_pending:${gameCode}`; - return await this.redisService.setMembers(key); - } - - // Redis methods for pending card answers - private async storePendingCard(gameCode: string, playerId: string, cardState: PendingCardState): Promise { - const key = `game_pending_card:${gameCode}:${playerId}`; - await this.redisService.setWithExpiry(key, JSON.stringify(cardState), 90); // 90 seconds (30 seconds buffer after timeout) - } - - private async getPendingCard(gameCode: string, playerId: string): Promise { - const key = `game_pending_card:${gameCode}:${playerId}`; - const dataStr = await this.redisService.get(key); - return dataStr ? JSON.parse(dataStr) : null; - } - - private async clearPendingCard(gameCode: string, playerId: string): Promise { - const key = `game_pending_card:${gameCode}:${playerId}`; - await this.redisService.del(key); - } - - // Redis methods for pending gamemaster decisions - private async storePendingDecision(gameCode: string, requestId: string, decisionState: PendingDecisionState): Promise { - const key = `game_pending_decision:${gameCode}:${requestId}`; - await this.redisService.setWithExpiry(key, JSON.stringify(decisionState), 150); // 150 seconds (30 seconds buffer after timeout) - } - - private async getPendingDecision(gameCode: string, requestId: string): Promise { - const key = `game_pending_decision:${gameCode}:${requestId}`; - const dataStr = await this.redisService.get(key); - return dataStr ? JSON.parse(dataStr) : null; - } - - private async clearPendingDecision(gameCode: string, requestId: string): Promise { - const key = `game_pending_decision:${gameCode}:${requestId}`; - await this.redisService.del(key); - } - - // Helper to get all pending decision keys for a game - private async getAllPendingDecisionKeys(gameCode: string): Promise { - // Note: This is a simplified version. In production, you might want to maintain a set of request IDs - // For now, we'll rely on the GamemasterService's in-memory tracking - const pendingDecisions = this.gamemasterService.getPendingDecisionsForGame(gameCode); - return pendingDecisions.map(d => d.requestId); - } - - private async getCurrentGameState(gameCode: string): Promise { - try { - const gamePlayKey = `gameplay:${gameCode}`; - const gameStateStr = await this.redisService.get(gamePlayKey); - - if (gameStateStr) { - return JSON.parse(gameStateStr); - } - return null; - } catch (error) { - logError('Error getting current game state', error as Error); - return null; - } - } - - private async getPlayerPositions(gameCode: string): Promise { - try { - // Get positions from gameplay (single source of truth) - const gameState = await this.getCurrentGameState(gameCode); - if (gameState && gameState.players) { - return gameState.players.map((player: any) => ({ - playerId: player.playerId, - playerName: player.playerName || player.playerId, - boardPosition: player.position || 0, - turnOrder: player.turnOrder - })); - } - - return []; - } catch (error) { - logError('Error getting player positions', error as Error); - return []; - } - } - - private async updatePlayerPosition(gameCode: string, playerId: string, newPosition: number): Promise { - try { - // Update position in gameplay (single source of truth) - const gameState = await this.getCurrentGameState(gameCode); - if (gameState && gameState.players) { - const player = gameState.players.find((p: any) => p.playerId === playerId); - if (player) { - player.position = newPosition; - - // Save updated gameplay state - const gamePlayKey = `gameplay:${gameCode}`; - await this.redisService.set(gamePlayKey, JSON.stringify(gameState)); - } - } - } catch (error) { - logError('Error updating player position', error as Error); - } - } - - // ============================================ - // TURN TRACKING REDIS METHODS - // ============================================ - - /** - * Set the number of extra turns for a player - */ - private async setPlayerExtraTurns( - gameCode: string, - playerId: string, - count: number - ): Promise { - const key = `player_extra_turns:${gameCode}:${playerId}`; - await this.redisService.set(key, count.toString()); - logOther(`Set extra turns for player ${playerId}`, { gameCode, count }); - } - - /** - * Get the number of extra turns for a player - */ - private async getPlayerExtraTurns( - gameCode: string, - playerId: string - ): Promise { - const key = `player_extra_turns:${gameCode}:${playerId}`; - const value = await this.redisService.get(key); - return value ? parseInt(value, 10) : 0; - } - - /** - * Decrement extra turns by 1, delete key if reaches 0 - */ - private async decrementPlayerExtraTurns( - gameCode: string, - playerId: string - ): Promise { - const current = await this.getPlayerExtraTurns(gameCode, playerId); - if (current > 1) { - await this.setPlayerExtraTurns(gameCode, playerId, current - 1); - } else { - const key = `player_extra_turns:${gameCode}:${playerId}`; - await this.redisService.del(key); - } - } - - /** - * Set the number of turns to lose for a player - */ - private async setPlayerTurnsToLose( - gameCode: string, - playerId: string, - count: number - ): Promise { - const key = `player_turns_to_lose:${gameCode}:${playerId}`; - await this.redisService.set(key, count.toString()); - logOther(`Set turns to lose for player ${playerId}`, { gameCode, count }); - } - - /** - * Get the number of turns to lose for a player - */ - private async getPlayerTurnsToLose( - gameCode: string, - playerId: string - ): Promise { - const key = `player_turns_to_lose:${gameCode}:${playerId}`; - const value = await this.redisService.get(key); - return value ? parseInt(value, 10) : 0; - } - - /** - * Decrement turns to lose by 1, delete key if reaches 0 - */ - private async decrementPlayerTurnsToLose( - gameCode: string, - playerId: string - ): Promise { - const current = await this.getPlayerTurnsToLose(gameCode, playerId); - if (current > 1) { - await this.setPlayerTurnsToLose(gameCode, playerId, current - 1); - } else { - const key = `player_turns_to_lose:${gameCode}:${playerId}`; - await this.redisService.del(key); - } - } - - /** - * Clear all turn tracking data for a player - */ - private async clearPlayerTurnData( - gameCode: string, - playerId: string - ): Promise { - await this.redisService.del(`player_extra_turns:${gameCode}:${playerId}`); - await this.redisService.del(`player_turns_to_lose:${gameCode}:${playerId}`); - } - - // ============================================ - // POSITION GUESSING MECHANICS - // ============================================ - - /** - * Determine if position guess is required based on field type and answer correctness - * - * Logic: - * - Positive field + correct answer = GUESS (reward scenario) - * - Positive field + wrong answer = NO GUESS (no movement) - * - Negative field + correct answer = NO GUESS (avoid penalty) - * - Negative field + wrong answer = GUESS (penalty scenario) - * - Regular field = NO GUESS (never guess on regular fields) - */ - private determineGuessRequirement( - fieldType: 'regular' | 'positive' | 'negative' | 'luck', - answerCorrect: boolean - ): boolean { - if (fieldType === 'positive') { - return answerCorrect; // Correct = guess for reward - } else if (fieldType === 'negative') { - return !answerCorrect; // Wrong = guess for penalty - } - return false; // Regular and luck fields never require guess - } - - /** - * Request position guess from player with stepping calculation info - */ - private async requestPositionGuess( - gameCode: string, - playerId: string, - playerName: string, - pendingState: PendingCardState - ): Promise { - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - // Calculate what the actual position would be (without showing to player yet) - // Use the LANDED field position for pattern modifier calculation - const landedFieldPosition = pendingState.field.position; - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - landedFieldPosition, - pendingState.field.stepValue || 0, - pendingState.dice - ); - - // Calculate the ACTUAL pattern modifier based on the LANDED field position - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(landedFieldPosition, positiveField); - - // Store stepping info for later validation - pendingState.requiresGuess = true; - const cardKey = `game_pending_card:${gameCode}:${playerId}`; - await this.redisService.setWithExpiry(cardKey, JSON.stringify(pendingState), 30); - - // Start timeout for position guess (30 seconds) - setTimeout(() => { - this.handlePositionGuessTimeout(gameCode, playerId, playerName, pendingState); - }, 30000); - - // Notify player to guess - send the ACTUAL pattern modifier and LANDED field position - this.io.of('/game').to(playerRoomName).emit('game:position-guess-request', { - message: 'Guess your final position!', - currentPosition: landedFieldPosition, - diceRoll: pendingState.dice, - fieldStepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - timeLimit: 30, - timestamp: new Date().toISOString() - }); - - // Notify others that player is guessing - this.io.of('/game').to(gameRoomName).emit('game:player-guessing', { - playerId, - playerName, - message: `${playerName} is guessing their final position...`, - timestamp: new Date().toISOString() - }); - - logOther(`Position guess requested from ${playerName}`, { gameCode }); - } - - /** - * Handle position guess timeout (player didn't guess in time) - */ - private async handlePositionGuessTimeout( - gameCode: string, - playerId: string, - playerName: string, - pendingState: PendingCardState - ): Promise { - try { - // Check if pending state still exists (player might have already guessed) - const cardKey = `game_pending_card:${gameCode}:${playerId}`; - const stateJson = await this.redisService.get(cardKey); - if (!stateJson) { - // Already processed, nothing to do - return; - } - - // Clear from Redis - await this.clearPendingCard(gameCode, playerId); - - const gameRoomName = `game_${gameCode}`; - - // Broadcast timeout to all players - this.io.of('/game').to(gameRoomName).emit('game:guess-timeout', { - playerId, - playerName, - message: `⏰ ${playerName} didn't guess in time!`, - timestamp: new Date().toISOString() - }); - - // Calculate actual position using LANDED field position - const landedFieldPosition = pendingState.field.position; - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - landedFieldPosition, - pendingState.field.stepValue || 0, - pendingState.dice - ); - - // Apply -2 penalty for timeout (treated as wrong guess) - const finalPosition = Math.max(1, actualPosition - 2); - - // Update player position - await this.updatePlayerPosition(gameCode, playerId, finalPosition); - - // Emit player-arrived event FIRST (before guess-result) - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: finalPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - // Calculate the actual pattern modifier used (based on LANDED field) - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(landedFieldPosition, positiveField); - - // Broadcast result - this.io.of('/game').to(gameRoomName).emit('game:guess-result', { - playerId, - playerName, - guessedPosition: null, - actualPosition: actualPosition, - finalPosition: finalPosition, - guessCorrect: false, - penaltyApplied: true, - calculation: { - startPosition: landedFieldPosition, - diceRoll: pendingState.dice, - stepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - calculatedPosition: actualPosition, - penalty: -2 - }, - message: `❌ ${playerName} timed out! Penalty applied. Final position: ${finalPosition}`, - timestamp: new Date().toISOString() - }); - - // Check for win condition - if (finalPosition >= 100) { - await this.endGame(gameCode, playerId, playerName); - return; - } - - // Check if landed on special field (secondary landing) - // For positive/negative fields, this will draw joker card - // For luck fields, this will return false and we'll handle them below - const secondaryLandingHandled = await this.checkSecondaryLanding( - gameCode, - playerId, - playerName, - finalPosition, - 0 // recursion depth - ); - - if (secondaryLandingHandled) { - // Joker card flow initiated, don't advance turn - return; - } - - // Check if landed on luck field (non-joker secondary landing) - const boardData = await this.getBoardData(gameCode); - if (boardData && boardData.fields) { - const landedField = boardData.fields.find((f: GameField) => f.position === finalPosition); - - if (landedField && landedField.type === 'luck') { - // Handle luck field normally - await this.handleSpecialFieldLanding( - gameCode, - playerId, - playerName, - landedField, - finalPosition, - 6, // Secondary landing uses dice = 6 - finalPosition // Use finalPosition as currentPosition for next card draw - ); - return; - } - } - - // No special field, advance turn - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error handling position guess timeout', error as Error); - // Don't call advanceTurn here - already called above if successful - } - } - - /** - * Handle position guess submission from player - */ - private async handlePositionGuess(socket: AuthenticatedSocket, data: { - gameCode: string; - guessedPosition: number; - }): Promise { - try { - const { gameCode, guessedPosition } = data; - const playerId = socket.userId || socket.playerName; - const playerName = socket.playerName; - - if (!playerId || !playerName) { - socket.emit('error', { message: 'Player identification failed' }); - return; - } - - // Get pending card state - const cardKey = `game_pending_card:${gameCode}:${playerId}`; - const stateJson = await this.redisService.get(cardKey); - if (!stateJson) { - socket.emit('error', { message: 'No pending guess found' }); - return; - } - - // Clear from Redis immediately to prevent timeout handler from processing - await this.clearPendingCard(gameCode, playerId); - - const pendingState: PendingCardState = JSON.parse(stateJson); - pendingState.guessedPosition = guessedPosition; - - // Broadcast the guess to everyone - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:position-guess-broadcast', { - playerId, - playerName, - guessedPosition, - message: `${playerName} guessed position ${guessedPosition}`, - timestamp: new Date().toISOString() - }); - - // Process the guess with FieldEffectService - await this.processQuestionCardWithGuess(gameCode, pendingState); - - // Clean up pending state - await this.redisService.del(cardKey); - - } catch (error) { - logError('Error handling position guess', error as Error); - socket.emit('error', { message: 'Failed to process guess' }); - } - } - - /** - * Process question card with guess using FieldEffectService - */ - private async processQuestionCardWithGuess( - gameCode: string, - pendingState: PendingCardState - ): Promise { - // Calculate actual position using BoardGenerationService - // Use the LANDED field position for calculation - const landedFieldPosition = pendingState.field.position; - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - landedFieldPosition, - pendingState.field.stepValue || 0, - pendingState.dice - ); - - let finalPosition = actualPosition; - let guessCorrect = false; - let penaltyApplied = false; - - // Check if guess was correct - if (pendingState.guessedPosition === actualPosition) { - guessCorrect = true; - } else { - // Wrong guess: apply -2 penalty - finalPosition = Math.max(1, actualPosition - 2); - penaltyApplied = true; - } - - // Update player position - await this.updatePlayerPosition(gameCode, pendingState.playerId, finalPosition); - - // Calculate the actual pattern modifier used (based on LANDED field) - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(landedFieldPosition, positiveField); - - // Emit player-arrived event FIRST (before guess-result) - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - position: finalPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - // Broadcast result - this.io.of('/game').to(gameRoomName).emit('game:guess-result', { - playerId: pendingState.playerId, - playerName: pendingState.playerName, - guessedPosition: pendingState.guessedPosition, - actualPosition: actualPosition, - finalPosition: finalPosition, - guessCorrect, - penaltyApplied, - calculation: { - startPosition: pendingState.field.position, - diceRoll: pendingState.dice, - stepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - calculatedPosition: actualPosition, - penalty: penaltyApplied ? -2 : 0 - }, - message: guessCorrect - ? `✅ ${pendingState.playerName} guessed correctly! Moved to ${finalPosition}` - : `❌ ${pendingState.playerName} guessed wrong! Penalty applied. Final position: ${finalPosition}`, - timestamp: new Date().toISOString() - }); - - // Check for win condition (position 100) - if (finalPosition >= 100) { - await this.endGame(gameCode, pendingState.playerId, pendingState.playerName); - return; - } - - // Check if landed on special field (secondary landing) - // For positive/negative fields, this will draw joker card - // For luck fields, this will return false and we'll handle them below - const secondaryLandingHandled = await this.checkSecondaryLanding( - gameCode, - pendingState.playerId, - pendingState.playerName, - finalPosition, - 0 // recursion depth - ); - - if (secondaryLandingHandled) { - // Joker card flow initiated, don't advance turn - return; - } - - // Check if landed on luck field (non-joker secondary landing) - const boardData = await this.getBoardData(gameCode); - if (boardData && boardData.fields) { - const landedField = boardData.fields.find((f: GameField) => f.position === finalPosition); - - if (landedField && landedField.type === 'luck') { - // Handle luck field normally - await this.handleSpecialFieldLanding( - gameCode, - pendingState.playerId, - pendingState.playerName, - landedField, - finalPosition, - 6, // Secondary landing uses dice = 6 - finalPosition // Use finalPosition as currentPosition for next card draw - ); - return; - } - } - - // No special field, advance turn - await this.advanceTurn(gameCode); - } - - /** - * Process luck card consequence with multi-turn support - */ - private async processLuckCard( - gameCode: string, - playerId: string, - playerName: string, - consequence: { type: number; value?: number }, - currentPosition: number - ): Promise { - const gameRoomName = `game_${gameCode}`; - let newPosition = currentPosition; - let shouldAdvanceTurn = true; - const consequenceValue = consequence.value || 1; - - // ConsequenceType enum: 0=MOVE_FORWARD, 1=MOVE_BACKWARD, 2=LOSE_TURN, 3=EXTRA_TURN, 5=GO_TO_START - switch (consequence.type) { - case 0: // MOVE_FORWARD - newPosition = Math.min(currentPosition + consequenceValue, 100); - await this.updatePlayerPosition(gameCode, playerId, newPosition); - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'MOVE_FORWARD', - value: consequenceValue, - newPosition, - message: `${playerName} moves forward ${consequenceValue} steps to position ${newPosition}!`, - timestamp: new Date().toISOString() - }); - // Emit player-arrived so frontend visualizes the movement - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'luck', - timestamp: new Date().toISOString() - }); - break; - - case 1: // MOVE_BACKWARD - newPosition = Math.max(1, currentPosition - consequenceValue); - await this.updatePlayerPosition(gameCode, playerId, newPosition); - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'MOVE_BACKWARD', - value: consequenceValue, - newPosition, - message: `${playerName} moves backward ${consequenceValue} steps to position ${newPosition}!`, - timestamp: new Date().toISOString() - }); - // Emit player-arrived so frontend visualizes the movement - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'luck', - timestamp: new Date().toISOString() - }); - break; - - case 2: // LOSE_TURN - // Store turns to lose in Redis - await this.setPlayerTurnsToLose(gameCode, playerId, consequenceValue); - - // Emit turn-lost event - this.io.of('/game').to(gameRoomName).emit('game:turn-lost', { - playerId, - playerName, - turnsToLose: consequenceValue, - message: `${playerName} will lose ${consequenceValue} turn(s)!`, - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'LOSE_TURN', - value: consequenceValue, - turnsToLose: consequenceValue, - message: `${playerName} will lose ${consequenceValue} turn(s)!`, - timestamp: new Date().toISOString() - }); - shouldAdvanceTurn = true; // Skip to next player immediately - break; - - case 3: // EXTRA_TURN - // Store extra turns in Redis - await this.setPlayerExtraTurns(gameCode, playerId, consequenceValue); - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'EXTRA_TURN', - value: consequenceValue, - extraTurns: consequenceValue, - message: `${playerName} gets ${consequenceValue} extra turn(s)!`, - timestamp: new Date().toISOString() - }); - shouldAdvanceTurn = true; // Let advanceTurn() handle extra turns - break; - - case 5: // GO_TO_START - newPosition = 1; - await this.updatePlayerPosition(gameCode, playerId, newPosition); - this.io.of('/game').to(gameRoomName).emit('game:luck-consequence', { - playerId, - playerName, - consequenceType: 'GO_TO_START', - value: consequenceValue, - newPosition: 1, - message: `${playerName} goes back to START!`, - timestamp: new Date().toISOString() - }); - break; - } - - // Check for win condition (position 100) - if (newPosition >= 100) { - await this.endGame(gameCode, playerId, playerName); - return; - } - - // Advance turn if needed - if (shouldAdvanceTurn) { - await this.advanceTurn(gameCode); - } - } - - // ============================================ - // JOKER CARD POSITION GUESSING - // ============================================ - - /** - * Request position guess from player AFTER gamemaster decision (for jokers) - */ - private async requestJokerPositionGuess( - gameCode: string, - playerId: string, - playerName: string, - pendingState: PendingDecisionState - ): Promise { - const gameRoomName = `game_${gameCode}`; - const playerRoomName = `game_${gameCode}:${playerName}`; - - // Calculate stepping info with dice = 6, using LANDED field position - const landedFieldPosition = pendingState.field.position; - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - landedFieldPosition, - pendingState.field.stepValue || 0, - 6 // Joker cards always use dice value of 6 - ); - - // Calculate the ACTUAL pattern modifier based on LANDED field position - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(landedFieldPosition, positiveField); - - // Update pending state - const decisionKey = `pending_decision:${gameCode}:${pendingState.playerId}`; - await this.redisService.setWithExpiry(decisionKey, JSON.stringify(pendingState), 30); - - // Start timeout for joker position guess (30 seconds) - setTimeout(() => { - this.handleJokerPositionGuessTimeout(gameCode, playerId, playerName, pendingState); - }, 30000); - - // Notify player to guess - send the ACTUAL pattern modifier and LANDED field position - this.io.of('/game').to(playerRoomName).emit('game:joker-position-guess-request', { - message: 'Guess your final position after joker!', - currentPosition: landedFieldPosition, - diceRoll: 6, - fieldStepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - timeLimit: 30, - timestamp: new Date().toISOString() - }); - - // Notify others - this.io.of('/game').to(gameRoomName).emit('game:player-guessing', { - playerId, - playerName, - message: `${playerName} is guessing their position after joker...`, - timestamp: new Date().toISOString() - }); - - logOther(`Joker position guess requested from ${playerName}`, { gameCode }); - } - - /** - * Handle joker position guess timeout (player didn't guess in time) - */ - private async handleJokerPositionGuessTimeout( - gameCode: string, - playerId: string, - playerName: string, - pendingState: PendingDecisionState - ): Promise { - try { - // Check if pending state still exists (player might have already guessed) - const decisionKey = `pending_decision:${gameCode}:${playerId}`; - const stateJson = await this.redisService.get(decisionKey); - if (!stateJson) { - // Already processed, nothing to do - return; - } - - // Clear from Redis - await this.clearPendingDecision(gameCode, playerId); - - const gameRoomName = `game_${gameCode}`; - - // Broadcast timeout to all players - this.io.of('/game').to(gameRoomName).emit('game:guess-timeout', { - playerId, - playerName, - message: `⏰ ${playerName} didn't guess in time after joker!`, - timestamp: new Date().toISOString() - }); - - // Default behavior on timeout: no movement (player stays at current position) - this.io.of('/game').to(gameRoomName).emit('game:joker-complete', { - playerId, - playerName, - guessedPosition: null, - actualPosition: pendingState.currentPosition, - finalPosition: pendingState.currentPosition, - guessCorrect: false, - penaltyApplied: false, - moved: false, - calculation: { - startPosition: pendingState.field.position, - diceRoll: 6, - stepValue: pendingState.field.stepValue || 0, - patternModifier: this.boardGenerationService.getPatternModifier(pendingState.field.position, (pendingState.field.stepValue || 0) > 0), - calculatedPosition: pendingState.currentPosition, - penalty: 0 - }, - message: `${playerName} timed out on joker guess (no movement)`, - timestamp: new Date().toISOString() - }); - - // Advance turn - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error handling joker position guess timeout', error as Error); - // Ensure turn advances even on error - await this.advanceTurn(gameCode); - } - } - - /** - * Handle joker position guess submission - */ - private async handleJokerPositionGuess(socket: AuthenticatedSocket, data: { - gameCode: string; - guessedPosition: number; - }): Promise { - try { - const { gameCode, guessedPosition } = data; - const playerId = socket.userId || socket.playerName; - const playerName = socket.playerName; - - if (!playerId || !playerName) { - socket.emit('error', { message: 'Player identification failed' }); - return; - } - - // Get pending decision state - try with playerId as key - let decisionKey = `pending_decision:${gameCode}:${playerId}`; - let stateJson = await this.redisService.get(decisionKey); - - if (!stateJson) { - socket.emit('error', { message: 'No pending joker guess found' }); - return; - } - - // Clear from Redis immediately to prevent timeout handler from processing - await this.clearPendingDecision(gameCode, playerId); - - const pendingState: PendingDecisionState = JSON.parse(stateJson); - pendingState.guessedPosition = guessedPosition; - - // Broadcast the guess - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:position-guess-broadcast', { - playerId, - playerName, - guessedPosition, - message: `${playerName} guessed position ${guessedPosition}`, - timestamp: new Date().toISOString() - }); - - // Calculate actual position using BoardGenerationService - const actualPosition = this.boardGenerationService.calculatePatternBasedMovement( - pendingState.currentPosition, - pendingState.field.stepValue || 0, - 6 // Joker always uses dice = 6 - ); - - let finalPosition = actualPosition; - let guessCorrect = false; - let penaltyApplied = false; - - // Check guess - if (guessedPosition === actualPosition) { - guessCorrect = true; - } else { - finalPosition = Math.max(1, actualPosition - 2); - penaltyApplied = true; - } - - // Apply movement based on field type and gamemaster decision - const shouldMove = (pendingState.field.type === 'positive' && pendingState.gamemasterApproved) || - (pendingState.field.type === 'negative' && !pendingState.gamemasterApproved); - - if (shouldMove) { - await this.updatePlayerPosition(gameCode, playerId, finalPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: finalPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - } - - // Calculate the actual pattern modifier used - const stepValue = pendingState.field.stepValue || 0; - const positiveField = stepValue > 0; - const actualPatternModifier = this.boardGenerationService.getPatternModifier(pendingState.currentPosition, positiveField); - - // Broadcast joker complete - this.io.of('/game').to(gameRoomName).emit('game:joker-complete', { - playerId, - playerName, - guessedPosition, - actualPosition: actualPosition, - finalPosition: shouldMove ? finalPosition : pendingState.currentPosition, - guessCorrect, - penaltyApplied, - moved: shouldMove, - calculation: { - startPosition: pendingState.field.position, - diceRoll: 6, - stepValue: pendingState.field.stepValue || 0, - patternModifier: actualPatternModifier, - calculatedPosition: actualPosition, - penalty: penaltyApplied ? -2 : 0 - }, - message: shouldMove - ? (guessCorrect - ? `✅ ${playerName} guessed correctly! Moved to ${finalPosition}` - : `❌ ${playerName} guessed wrong! Penalty applied. Final position: ${finalPosition}`) - : `${playerName} did not move (gamemaster decision)`, - timestamp: new Date().toISOString() - }); - - // Clean up - await this.redisService.del(decisionKey); - - // Check for win condition - const movedPosition = shouldMove ? finalPosition : pendingState.currentPosition; - if (movedPosition >= 100) { - await this.endGame(gameCode, playerId, playerName); - return; - } - - // Check if landed on special field (secondary landing) if moved - if (shouldMove) { - // For positive/negative fields, this will draw joker card - // For luck fields, this will return false and we'll handle them below - const secondaryLandingHandled = await this.checkSecondaryLanding( - gameCode, - playerId, - playerName, - finalPosition, - 0 // recursion depth - ); - - if (secondaryLandingHandled) { - // Joker card flow initiated, don't advance turn - return; - } - - // Check if landed on luck field (non-joker secondary landing) - const boardData = await this.getBoardData(gameCode); - if (boardData && boardData.fields) { - const landedField = boardData.fields.find((f: GameField) => f.position === finalPosition); - - if (landedField && landedField.type === 'luck') { - // Handle luck field normally - await this.handleSpecialFieldLanding( - gameCode, - playerId, - playerName, - landedField, - finalPosition, - 6, // Secondary landing uses dice = 6 - finalPosition // Use finalPosition as currentPosition for next card draw - ); - return; - } - } - } - - // No special field or didn't move, advance turn - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error handling joker position guess', error as Error); - socket.emit('error', { message: 'Failed to process joker guess' }); - } - } - - private async advanceTurn(gameCode: string): Promise { - try { - const gameState = await this.getCurrentGameState(gameCode); - if (!gameState) return; - - const currentTurnIndex = gameState.currentTurn || 0; - const currentPlayerId = gameState.turnSequence[currentTurnIndex]; - - // ========================================== - // PHASE 1: Check if current player has extra turns - // ========================================== - const extraTurns = await this.getPlayerExtraTurns(gameCode, currentPlayerId); - if (extraTurns > 0) { - // Current player gets another turn - await this.decrementPlayerExtraTurns(gameCode, currentPlayerId); - - const playerPositions = await this.getPlayerPositions(gameCode); - const currentPlayer = playerPositions.find(p => p.playerId === currentPlayerId); - const currentPlayerName = currentPlayer?.playerName || currentPlayerId; - - // Notify about extra turn - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:extra-turn-remaining', { - playerId: currentPlayerId, - playerName: currentPlayerName, - remainingExtraTurns: extraTurns - 1, - message: `${currentPlayerName} has ${extraTurns - 1} extra turn(s) remaining!`, - timestamp: new Date().toISOString() - }); - - // Notify player they can roll again - const playerRoomName = `game_${gameCode}:${currentPlayerName}`; - this.io.of('/game').to(playerRoomName).emit('game:your-turn', { - message: 'Extra turn! Roll the dice again!', - canRoll: true, - isExtraTurn: true, - timestamp: new Date().toISOString() - }); - - logOther(`Player ${currentPlayerName} using extra turn`, { - gameCode, - remainingExtraTurns: extraTurns - 1 - }); - - return; // Same player continues, don't advance - } - - // ========================================== - // PHASE 2: Find next player, skipping those with lost turns - // ========================================== - let nextTurnIndex = (currentTurnIndex + 1) % gameState.turnSequence.length; - const skippedPlayers: Array<{ - playerId: string; - playerName: string; - remainingTurnsToLose: number; - }> = []; - let loopGuard = 0; - const maxLoops = gameState.turnSequence.length; - - while (loopGuard < maxLoops) { - const candidatePlayerId = gameState.turnSequence[nextTurnIndex]; - const turnsToLose = await this.getPlayerTurnsToLose(gameCode, candidatePlayerId); - - if (turnsToLose > 0) { - // This player loses their turn - await this.decrementPlayerTurnsToLose(gameCode, candidatePlayerId); - - const playerPositions = await this.getPlayerPositions(gameCode); - const skippedPlayer = playerPositions.find(p => p.playerId === candidatePlayerId); - const skippedPlayerName = skippedPlayer?.playerName || candidatePlayerId; - - skippedPlayers.push({ - playerId: candidatePlayerId, - playerName: skippedPlayerName, - remainingTurnsToLose: turnsToLose - 1 - }); - - logOther(`Player ${skippedPlayerName} turn skipped`, { - gameCode, - remainingTurnsToLose: turnsToLose - 1 - }); - - // Move to next player in sequence - nextTurnIndex = (nextTurnIndex + 1) % gameState.turnSequence.length; - loopGuard++; - } else { - // Found a player who can play - break; - } - } - - // ========================================== - // PHASE 3: Update game state with valid next player - // ========================================== - const nextPlayerId = gameState.turnSequence[nextTurnIndex]; - gameState.currentTurn = nextTurnIndex; - gameState.currentPlayer = nextPlayerId; - - // Save updated state - const gamePlayKey = `gameplay:${gameCode}`; - await this.redisService.set(gamePlayKey, JSON.stringify(gameState)); - - // Create snapshot every 5 turns - const newTurnNumber = nextTurnIndex + 1; - if (this.gameSnapshotService.shouldCreateSnapshot(newTurnNumber)) { - await this.gameSnapshotService.createSnapshot( - gameCode, - newTurnNumber, - SnapshotTrigger.TURN_INTERVAL, - `Automatic snapshot at turn ${newTurnNumber}` - ).catch(err => { - logError('Failed to create turn snapshot', err as Error); - logOther('Turn snapshot context', { gameCode, turnNumber: newTurnNumber }); - }); - } - - // Get next player info - const playerPositions = await this.getPlayerPositions(gameCode); - const nextPlayer = playerPositions.find(p => p.playerId === nextPlayerId); - const nextPlayerName = nextPlayer?.playerName || nextPlayerId; - - // ========================================== - // PHASE 4: Notify about skipped players (if any) - // ========================================== - const gameRoomName = `game_${gameCode}`; - if (skippedPlayers.length > 0) { - this.io.of('/game').to(gameRoomName).emit('game:players-skipped', { - skippedPlayers, - message: `${skippedPlayers.map(p => p.playerName).join(', ')} skipped due to lost turn(s)`, - timestamp: new Date().toISOString() - }); - } - - // ========================================== - // PHASE 5: Notify about turn change - // ========================================== - this.io.of('/game').to(gameRoomName).emit('game:turn-changed', { - currentPlayer: nextPlayerId, - currentPlayerName: nextPlayerName, - turnNumber: nextTurnIndex + 1, - message: `It's ${nextPlayerName}'s turn!`, - timestamp: new Date().toISOString() - }); - - // Send special notification to the current player - const playerRoomName = `game_${gameCode}:${nextPlayerName}`; - this.io.of('/game').to(playerRoomName).emit('game:your-turn', { - message: 'It\'s your turn! Roll the dice!', - canRoll: true, - timestamp: new Date().toISOString() - }); - - logOther(`Turn advanced in game ${gameCode}`, { - previousTurn: currentTurnIndex, - newTurn: nextTurnIndex, - nextPlayer: nextPlayerName, - skippedCount: skippedPlayers.length - }); - - } catch (error) { - logError('Error advancing turn', error as Error); - } - } - - private async endGame(gameCode: string, winnerId: string, winnerName: string): Promise { - try { - // Update game state to finished - const gameState = await this.getCurrentGameState(gameCode); - if (gameState) { - gameState.gamePhase = 'finished'; - gameState.winner = winnerId; - gameState.winnerName = winnerName; - gameState.endedAt = new Date().toISOString(); - - const gamePlayKey = `gameplay:${gameCode}`; - await this.redisService.set(gamePlayKey, JSON.stringify(gameState)); - } - - // Update database game record - const game = await this.gameRepository.findByGameCode(gameCode); - if (game) { - await this.gameRepository.update(game.id, { - state: GameState.FINISHED, - winnerId: winnerId, - enddate: new Date() - }); - } - - // Broadcast game end to all players - const gameRoomName = `game_${gameCode}`; - this.io.of('/game').to(gameRoomName).emit('game:ended', { - winner: winnerId, - winnerName: winnerName, - message: `🎉 ${winnerName} won the game! Congratulations!`, - finalPositions: await this.getPlayerPositions(gameCode), - timestamp: new Date().toISOString() - }); - - // Clean up all game-related Redis data and socket connections - await this.cleanupGameData(gameCode, game?.id); - - logOther(`Game ${gameCode} ended and cleaned up`, { - winner: winnerName, - winnerId, - gameId: game?.id - }); - - } catch (error) { - logError('Error ending game', error as Error); - } - } - - private async checkAllPlayersReady(gameCode: string): Promise { - try { - // Get connected players from Redis - const connectedPlayers = await this.getConnectedPlayers(gameCode); - const readyPlayers = await this.getReadyPlayers(gameCode); - - // All connected players must be ready for the game to start - return readyPlayers.length === connectedPlayers.length && connectedPlayers.length > 1; - } catch (error) { - logError('Error checking if all players are ready', error as Error); - return false; - } - } - - /** - * Apply card consequence (movement, turn effects) to a player - */ - private async applyCardConsequence(gameCode: string, playerId: string, playerName: string, consequence: number, recursionDepth: number = 0): Promise { - try { - // Safety check: prevent infinite loops - const MAX_RECURSION_DEPTH = 5; - if (recursionDepth >= MAX_RECURSION_DEPTH) { - logWarning(`Max recursion depth reached for consequence application in game ${gameCode}`); - await this.advanceTurn(gameCode); - return; - } - - // ConsequenceType enum: - // 0: MOVE_FORWARD, 1: MOVE_BACKWARD, 2: LOSE_TURN, 3: EXTRA_TURN, 5: GO_TO_START - - const positions = await this.getPlayerPositions(gameCode); - const currentPlayer = positions.find(p => p.playerId === playerId); - - if (!currentPlayer) { - logWarning(`Player ${playerId} not found when applying consequence`); - return; - } - - const gameRoomName = `game_${gameCode}`; - let newPosition = currentPlayer.boardPosition; - let positionChanged = false; - - switch (consequence) { - case 0: // MOVE_FORWARD - newPosition = Math.min(currentPlayer.boardPosition + 3, 101); // Move forward 3 steps - positionChanged = newPosition !== currentPlayer.boardPosition; - await this.updatePlayerPosition(gameCode, playerId, newPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'move_forward', - oldPosition: currentPlayer.boardPosition, - newPosition, - timestamp: new Date().toISOString() - }); - break; - - case 1: // MOVE_BACKWARD - newPosition = Math.max(currentPlayer.boardPosition - 3, 0); // Move backward 3 steps - positionChanged = newPosition !== currentPlayer.boardPosition; - await this.updatePlayerPosition(gameCode, playerId, newPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'move_backward', - oldPosition: currentPlayer.boardPosition, - newPosition, - timestamp: new Date().toISOString() - }); - break; - - case 2: // LOSE_TURN - // Immediately advance to next player - await this.advanceTurn(gameCode); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'lose_turn', - timestamp: new Date().toISOString() - }); - return; // Early return, turn already advanced - break; - - case 3: // EXTRA_TURN - // Don't advance turn, player gets to go again - const playerRoomName = `game_${gameCode}:${playerName}`; - this.io.of('/game').to(playerRoomName).emit('game:extra-turn', { - message: 'You get an extra turn!', - canRoll: true, - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'extra_turn', - timestamp: new Date().toISOString() - }); - return; // Early return, no turn advance needed - break; - - case 5: // GO_TO_START - newPosition = 0; - positionChanged = newPosition !== currentPlayer.boardPosition; - await this.updatePlayerPosition(gameCode, playerId, newPosition); - - // Emit player-arrived event - this.io.of('/game').to(gameRoomName).emit('game:player-arrived', { - playerId, - playerName, - position: newPosition, - fieldType: 'normal', - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(gameRoomName).emit('game:consequence-applied', { - playerName, - playerId, - type: 'go_to_start', - oldPosition: currentPlayer.boardPosition, - newPosition, - timestamp: new Date().toISOString() - }); - break; - - default: - logWarning(`Unknown consequence type: ${consequence}`); - } - - // Check for secondary special field landing (only if position changed) - if (positionChanged && newPosition > 0 && newPosition < 101) { - const secondaryLanding = await this.checkSecondaryLanding(gameCode, playerId, playerName, newPosition, recursionDepth); - if (secondaryLanding) { - // Secondary landing detected, joker flow initiated, don't advance turn yet - return; - } - } - - // If no secondary landing, advance to next player - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error applying card consequence', error as Error); - } - } - - /** - * Check if player landed on special field as result of consequence - * Returns true if joker card flow was initiated, false otherwise - */ - private async checkSecondaryLanding(gameCode: string, playerId: string, playerName: string, position: number, recursionDepth: number): Promise { - try { - logOther(`🔍 Checking secondary landing for ${playerName} at position ${position}`, { - gameCode, - playerId, - playerName, - position, - recursionDepth - }); - - const boardData = await this.getBoardData(gameCode); - if (!boardData || !boardData.fields) { - logOther('❌ No board data found for secondary landing check'); - return false; - } - - const landedField = boardData.fields.find((f: GameField) => f.position === position); - - // Check if field is special (positive or negative only for joker) - if (!landedField || !this.isSpecialField(landedField)) { - logOther(`❌ Position ${position} is not a special field`, { - hasField: !!landedField, - fieldType: landedField?.type - }); - return false; - } - - // Only positive and negative fields trigger joker on secondary landing - if (landedField.type !== 'positive' && landedField.type !== 'negative') { - logOther(`❌ Field type ${landedField.type} does not trigger joker (only positive/negative do)`); - return false; - } - - logOther(`✅ Secondary landing detected on ${landedField.type} field at position ${position} - Drawing joker card!`, { - fieldType: landedField.type, - position - }); - - const gameRoomName = `game_${gameCode}`; - - // Notify players about secondary landing - this.io.of('/game').to(gameRoomName).emit('game:secondary-landing', { - playerName, - playerId, - position, - fieldType: landedField.type, - message: `${playerName} landed on a ${landedField.type} field! Drawing joker card...`, - timestamp: new Date().toISOString() - }); - - // Wait 2 seconds for animation - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Draw joker card - await this.handleJokerCardDrawing(gameCode, playerId, playerName, landedField, position, recursionDepth); - - return true; // Joker flow initiated - - } catch (error) { - logError('Error checking secondary landing', error as Error); - return false; - } - } - - /** - * Handle joker card drawing and gamemaster decision flow - */ - private async handleJokerCardDrawing(gameCode: string, playerId: string, playerName: string, field: GameField, position: number, recursionDepth: number): Promise { - try { - const gameRoomName = `game_${gameCode}`; - - // Get game data - const gameData = await this.gameRepository.findByGameCode(gameCode); - if (!gameData) { - logError('Game not found when drawing joker card'); - await this.advanceTurn(gameCode); - return; - } - - // Draw joker card - const jokerResult = this.cardDrawingService.drawJokerCard(gameData, playerId); - - if (!jokerResult.success || !jokerResult.card) { - // No more joker cards available - this.io.of('/game').to(gameRoomName).emit('game:joker-error', { - playerName, - playerId, - error: jokerResult.error || 'No joker cards available', - timestamp: new Date().toISOString() - }); - await this.advanceTurn(gameCode); - return; - } - - const jokerCard = jokerResult.card; - - // Emit joker-activated event - this.io.of('/game').to(gameRoomName).emit('game:joker-activated', { - playerName, - playerId, - message: `${playerName} activated a Joker card!`, - timestamp: new Date().toISOString() - }); - - // Broadcast joker drawn to all players - this.io.of('/game').to(gameRoomName).emit('game:joker-drawn', { - playerName, - playerId, - jokerCard: { - question: jokerCard.question, - consequence: jokerCard.consequence - }, - waitingForGamemaster: true, - timestamp: new Date().toISOString() - }); - - // Request gamemaster decision - const requestId = this.gamemasterService.requestGamemasterDecision( - gameCode, - playerId, - playerName, - jokerCard, - (reqId: string) => this.handleGamemasterDecisionTimeout(gameCode, reqId, playerId, playerName, recursionDepth) - ); - - // Store pending decision in Redis - await this.storePendingDecision(gameCode, requestId, { - playerId, - playerName, - card: jokerCard, - field: field, - dice: 6, // Joker cards always use dice value of 6 - currentPosition: position, // Current position is where they landed - drawnAt: Date.now(), - recursionDepth - }); - - // Find gamemaster - const gamemaster = gameData.createdby; - const gamemasterUser = await this.userRepository.findById(gamemaster); - const gamemasterName = gamemasterUser?.username || 'Gamemaster'; - - // Send decision request to gamemaster only - const gamemasterRoomName = `game_${gameCode}:${gamemasterName}`; - this.io.of('/game').to(gamemasterRoomName).emit('game:gamemaster-decision-request', { - requestId, - playerName, - playerId, - jokerCard: { - question: jokerCard.question, - consequence: jokerCard.consequence - }, - timeLimit: 120, // 120 seconds - recursionDepth, // Send to frontend for context - timestamp: new Date().toISOString() - }); - - logOther(`Joker card drawn for ${playerName}, waiting for gamemaster decision`, { - gameCode, - requestId, - recursionDepth - }); - - } catch (error) { - logError('Error handling joker card drawing', error as Error); - await this.advanceTurn(gameCode); - } - } - - /** - * Handle gamemaster decision timeout (120 seconds elapsed) - */ - private async handleGamemasterDecisionTimeout(gameCode: string, requestId: string, playerId: string, playerName: string, recursionDepth: number): Promise { - try { - // Clear from Redis - await this.clearPendingDecision(gameCode, requestId); - - const gameRoomName = `game_${gameCode}`; - - // Broadcast timeout to all players - this.io.of('/game').to(gameRoomName).emit('game:gamemaster-timeout', { - playerName, - playerId, - message: '🎭 Gamemaster didn\'t respond in time. No effect applied.', - timestamp: new Date().toISOString() - }); - - // Process timeout through GamemasterService - const result = this.gamemasterService.processTimeoutDecision(requestId); - - if (result) { - // Broadcast final result - this.io.of('/game').to(gameRoomName).emit('game:gamemaster-decision-result', { - playerName, - playerId, - gamemasterName: 'System (timeout)', - decision: 'timeout', - consequence: result.consequence, - description: result.description, - timestamp: new Date().toISOString() - }); - } - - // Advance turn (no consequence applied on timeout) - await this.advanceTurn(gameCode); - - } catch (error) { - logError('Error handling gamemaster decision timeout', error as Error); - await this.advanceTurn(gameCode); - } - } - - // Public method to broadcast game state updates from external services - public async broadcastGameStateUpdate(gameCode: string, gameState: any): Promise { - const roomName = `game_${gameCode}`; - this.io.of('/game').to(roomName).emit('game:state-update', gameState); - } - - // Public method to broadcast game events from external services - public async broadcastGameEvent(gameCode: string, event: string, data: any): Promise { - const roomName = `game_${gameCode}`; - this.io.of('/game').to(roomName).emit(event, data); - } - - // Public method to send events to a specific player - public async sendToPlayer(gameCode: string, playerName: string, event: string, data: any): Promise { - const playerRoomName = `game_${gameCode}:${playerName}`; - this.io.of('/game').to(playerRoomName).emit(event, data); - logOther(`Sent event '${event}' to player ${playerName} in game ${gameCode}`); - } - - // Public method to send events to multiple specific players - public async sendToPlayers(gameCode: string, playerNames: string[], event: string, data: any): Promise { - for (const playerName of playerNames) { - await this.sendToPlayer(gameCode, playerName, event, data); - } - } - - // Public method to get connected players in a game - public async getConnectedPlayers(gameCode: string): Promise { - const key = `game_connections:${gameCode}`; - return await this.redisService.setMembers(key); - } - - // Public method to get ready players in a game - public async getReadyPlayers(gameCode: string): Promise { - const key = `game_ready:${gameCode}`; - return await this.redisService.setMembers(key); - } - - // Public method to broadcast game start with board data and player order - public async broadcastGameStart(gameCode: string, boardData: any, playerOrder: string[], gameData: any): Promise { - try { - const roomName = `game_${gameCode}`; - - // Create comprehensive game start data - const gameStartData = { - gameCode, - gameId: gameData.id, - status: 'started', - boardData, - playerOrder, - currentPlayer: playerOrder[0], // First player starts - currentTurn: 0, - maxPlayers: gameData.maxplayers, - players: gameData.players, - startedAt: new Date().toISOString(), - message: 'Game has started! Good luck to all players!' - }; - - // Broadcast to all players in the game - this.io.of('/game').to(roomName).emit('game:start', gameStartData); - - // Note: Game state is already stored in gameplay:{gameCode} - no need for duplicate game_state: entry - - // Initialize player positions (all start at 0) - const playerPositions = await this.getPlayerPositions(gameCode); - - // Notify the first player that it's their turn - const firstPlayerName = playerPositions.find(p => p.playerId === playerOrder[0])?.playerName || playerOrder[0]; - const firstPlayerRoomName = `game_${gameCode}:${firstPlayerName}`; - - // Send turn-changed event for initial turn with player name - this.io.of('/game').to(roomName).emit('game:turn-changed', { - currentPlayer: playerOrder[0], - currentPlayerName: firstPlayerName, - turnNumber: 1, - message: `It's ${firstPlayerName}'s turn!`, - timestamp: new Date().toISOString() - }); - - this.io.of('/game').to(firstPlayerRoomName).emit('game:your-turn', { - message: 'You go first! Roll the dice to start the game!', - canRoll: true, - timestamp: new Date().toISOString() - }); - - logOther(`Game start broadcasted to all players in room: ${roomName}`, { - gameCode, - gameId: gameData.id, - playerCount: gameData.players.length, - boardFields: boardData?.fields?.length || 0, - firstPlayer: playerOrder[0], - firstPlayerName - }); - - } catch (error) { - logError('Error broadcasting game start', error as Error); - throw error; // Re-throw so the caller knows the broadcast failed - } - } - - /** - * Comprehensive cleanup of all game-related data when game ends - * @param gameCode Game code - * @param gameId Game ID from database - */ - private async cleanupGameData(gameCode: string, gameId?: string): Promise { - try { - logOther(`Starting cleanup for game ${gameCode}`, { gameId }); - - // 1. Force disconnect all players from game rooms - const gameRoomName = `game_${gameCode}`; - const gameRoom = this.io.of('/game').adapter.rooms.get(gameRoomName); - - if (gameRoom) { - // Get all socket IDs in the room - const socketIds = Array.from(gameRoom); - - for (const socketId of socketIds) { - const socket = this.io.of('/game').sockets.get(socketId); - if (socket) { - // Leave game rooms - await socket.leave(gameRoomName); - await socket.leave(`game_${gameCode}:${(socket as any).playerName}`); - - // Clear game-related socket data - (socket as any).gameCode = undefined; - (socket as any).playerName = undefined; - - // Notify player that game has ended - socket.emit('game:cleanup-complete', { - gameCode, - message: 'Game session has ended and been cleaned up', - timestamp: new Date().toISOString() - }); - } - } - } - - // 2. Clean up all Redis game data - const keysToClean = [ - `gameplay:${gameCode}`, // Game play state (contains everything) - `game_connections:${gameCode}`, // Connected players - `game_ready:${gameCode}`, // Ready players - `game_pending:${gameCode}`, // Pending players (for private games) - `game_room:${gameCode}` // Game room mapping - ]; - - // Clean up legacy keys if they exist - if (gameId) { - keysToClean.push(`game_board_${gameId}`); // Legacy board storage - keysToClean.push(`game_state:${gameCode}`); // Legacy game state - } - - // Clean up game-specific keys - for (const key of keysToClean) { - await this.redisService.del(key); - } - - // Clean up all pending card answers for this game - const connectedPlayers = await this.getConnectedPlayers(gameCode); - for (const playerId of connectedPlayers) { - await this.clearPendingCard(gameCode, playerId); - } - - // Clean up all pending gamemaster decisions - const pendingDecisionIds = await this.getAllPendingDecisionKeys(gameCode); - for (const requestId of pendingDecisionIds) { - await this.clearPendingDecision(gameCode, requestId); - // Also cancel in GamemasterService to clear timeouts - this.gamemasterService.cancelDecision(requestId); - } - - // Clean up turn tracking for all players - const gameState = await this.getCurrentGameState(gameCode); - if (gameState?.turnSequence) { - for (const playerId of gameState.turnSequence) { - await this.clearPlayerTurnData(gameCode, playerId); - } - } - - // Clean up additional game keys - const additionalKeys = [ - `game:${gameCode}` // Pre-game lobby data (uses gameCode not gameId) - ]; - - for (const key of additionalKeys) { - await this.redisService.del(key); - } - - logOther(`Game cleanup completed for ${gameCode}`, { - gameId, - keysCleanedCount: keysToClean.length + (gameId ? 3 : 0) - }); - - } catch (error) { - logError('Error during game cleanup', error as Error); - logOther('Game cleanup failed', { gameCode, gameId, errorMessage: error instanceof Error ? error.message : String(error) }); - } - } - - /** - * Public method to manually trigger game cleanup (for external services) - * @param gameCode Game code to clean up - * @param gameId Optional game ID - */ - public async triggerGameCleanup(gameCode: string, gameId?: string): Promise { - logOther(`Manual cleanup triggered for game ${gameCode}`, { gameId }); - await this.cleanupGameData(gameCode, gameId); - } - - /** - * Get board data for a game from Redis - * Board data is stored in gameplay:{gameCode} - */ - private async getBoardData(gameCode: string): Promise { - try { - // Get from gameplay (single source of truth) - const gameState = await this.getCurrentGameState(gameCode); - return gameState?.boardData || null; - - } catch (error) { - logError('Error getting board data', error as Error); - return null; - } - } - - /** - * Check if field is special (requires card drawing) - * @param field Game field to check - * @returns True if field is special - */ - private isSpecialField(field: GameField): boolean { - return field.type === 'positive' || field.type === 'negative' || field.type === 'luck'; - } - - /** - * Check if card is a luck card - * @param cardType Card type - * @returns True if luck card - */ - private isLuckCard(cardType?: number): boolean { - return cardType === 6; // Luck cards have type 6 - } - - /** - * Create snapshots for all active games (called on server shutdown) - * @returns Number of snapshots created - */ - public async snapshotAllActiveGames(): Promise { - try { - logOther('Creating snapshots for all active games before shutdown'); - - // Find all active games from database - const activeGames = await this.gameRepository.findActiveGames(); - let snapshotCount = 0; - - for (const game of activeGames) { - try { - // Get current game state from Redis - const gameState = await this.getCurrentGameState(game.gamecode); - if (!gameState) { - logOther(`Skipping game ${game.gamecode} - no Redis state found`); - continue; - } - - const turnNumber = (gameState.currentTurn || 0) + 1; - await this.gameSnapshotService.createSnapshot( - game.gamecode, - turnNumber, - SnapshotTrigger.SERVER_SHUTDOWN, - `Server shutdown snapshot` - ); - snapshotCount++; - logOther(`Created shutdown snapshot for game ${game.gamecode}`, { turnNumber }); - } catch (gameError) { - logError(`Failed to snapshot game ${game.gamecode}`, gameError as Error); - } - } - - logOther(`Completed shutdown snapshots`, { totalGames: activeGames.length, successfulSnapshots: snapshotCount }); - return snapshotCount; - } catch (error) { - logError('Error creating shutdown snapshots', error as Error); - return 0; - } - } - - /** - * Restore all active games from latest snapshots (called on server startup) - * @returns Number of games restored - */ - public async restoreAllActiveGames(): Promise { - try { - logOther('Attempting to restore active games from snapshots'); - - // Find all active games from database - const activeGames = await this.gameRepository.findActiveGames(); - let restoredCount = 0; - - for (const game of activeGames) { - try { - // Try to restore from latest snapshot - const restored = await this.gameSnapshotService.restoreFromSnapshot(game.gamecode); - if (restored) { - restoredCount++; - logOther(`Restored game ${game.gamecode} from snapshot`); - } else { - logOther(`No snapshot found for game ${game.gamecode}, skipping`); - } - } catch (gameError) { - logError(`Failed to restore game ${game.gamecode}`, gameError as Error); - } - } - - logOther(`Completed game restoration`, { totalGames: activeGames.length, restoredGames: restoredCount }); - return restoredCount; - } catch (error) { - logError('Error restoring games from snapshots', error as Error); - return 0; - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/GamemasterService.ts b/SerpentRace_Backend/src/Application/Services/GamemasterService.ts deleted file mode 100644 index 896ea45b..00000000 --- a/SerpentRace_Backend/src/Application/Services/GamemasterService.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { GameAggregate, GameCard } from '../../Domain/Game/GameAggregate'; - -export interface GamemasterDecisionRequest { - gameId: string; - playerId: string; - playerName: string; - card: GameCard; - requestId: string; - timeoutId: NodeJS.Timeout; - startTime: Date; -} - -export enum GamemasterDecision { - APPROVE = 'approve', - REJECT = 'reject' -} - -export interface GamemasterDecisionResult { - decision: GamemasterDecision; - consequence: boolean; // true = apply consequence, false = don't apply - description: string; -} - -/** - * Service responsible for handling gamemaster decisions on joker cards - * Integrates with existing gamemaster role identification system - */ -export class GamemasterService { - private pendingDecisions: Map = new Map(); - private readonly DECISION_TIMEOUT_MS = 120000; // 2 minutes for gamemaster to decide - - /** - * Request gamemaster decision for a joker card - * @param gameId Game ID - * @param playerId Player ID who drew the joker card - * @param playerName Player name for display - * @param card The joker card that needs decision - * @param onTimeout Callback when gamemaster doesn't respond in time - * @returns Request ID for tracking this decision - */ - requestGamemasterDecision( - gameId: string, - playerId: string, - playerName: string, - card: GameCard, - onTimeout: (requestId: string) => void - ): string { - const requestId = `${gameId}:${playerId}:${Date.now()}`; - - // Clear any existing decision for this player - this.clearExistingDecision(gameId, playerId); - - // Set timeout for gamemaster decision - const timeoutId = setTimeout(() => { - onTimeout(requestId); - this.pendingDecisions.delete(requestId); - }, this.DECISION_TIMEOUT_MS); - - // Store pending decision - this.pendingDecisions.set(requestId, { - gameId, - playerId, - playerName, - card, - requestId, - timeoutId, - startTime: new Date() - }); - - return requestId; - } - - /** - * Process gamemaster's decision on a joker card - * @param requestId The request ID returned from requestGamemasterDecision - * @param decision The gamemaster's decision - * @returns Result with consequence application info - */ - processGamemasterDecision(requestId: string, decision: GamemasterDecision): GamemasterDecisionResult | null { - const pendingRequest = this.pendingDecisions.get(requestId); - - if (!pendingRequest) { - return null; // Request not found or already processed - } - - // Clear the timeout since decision was made - clearTimeout(pendingRequest.timeoutId); - this.pendingDecisions.delete(requestId); - - // Determine if consequence should be applied based on its nature and decision - const consequence = pendingRequest.card.consequence; - const isNegativeConsequence = this.isNegativeConsequence(consequence?.type); - - let applyConsequence: boolean; - if (isNegativeConsequence) { - // Negative consequences applied when gamemaster REJECTS - applyConsequence = decision === GamemasterDecision.REJECT; - } else { - // Positive consequences applied when gamemaster APPROVES - applyConsequence = decision === GamemasterDecision.APPROVE; - } - - return { - decision, - consequence: applyConsequence, - description: this.getDecisionDescription(decision, applyConsequence, pendingRequest.card, isNegativeConsequence) - }; - } - - /** - * Process automatic decision when gamemaster times out - * @param requestId The request ID that timed out - * @returns Result with default rejection applied - */ - processTimeoutDecision(requestId: string): GamemasterDecisionResult | null { - const pendingRequest = this.pendingDecisions.get(requestId); - - if (!pendingRequest) { - return null; - } - - this.pendingDecisions.delete(requestId); - - return { - decision: GamemasterDecision.REJECT, - consequence: false, - description: `🎭 Gamemaster didn't respond in time. No effect applied.` - }; - } - - /** - * Get pending decision by request ID - * @param requestId The request ID - * @returns Pending decision request or undefined - */ - getPendingDecision(requestId: string): GamemasterDecisionRequest | undefined { - return this.pendingDecisions.get(requestId); - } - - /** - * Get all pending decisions for a game - * @param gameId Game ID - * @returns Array of pending decisions for the game - */ - getPendingDecisionsForGame(gameId: string): GamemasterDecisionRequest[] { - return Array.from(this.pendingDecisions.values()) - .filter(request => request.gameId === gameId); - } - - /** - * Check if gamemaster is the correct user for a game - * @param game Game aggregate - * @param userId User ID to check - * @returns True if user is the gamemaster - */ - isGamemaster(game: GameAggregate, userId: string): boolean { - return game.createdby === userId; - } - - /** - * Cancel a pending decision (e.g., if player leaves game) - * @param requestId Request ID to cancel - * @returns True if decision was cancelled - */ - cancelDecision(requestId: string): boolean { - const pendingRequest = this.pendingDecisions.get(requestId); - - if (!pendingRequest) { - return false; - } - - clearTimeout(pendingRequest.timeoutId); - this.pendingDecisions.delete(requestId); - return true; - } - - /** - * Clear any existing pending decision for a player in a game - * @param gameId Game ID - * @param playerId Player ID - */ - private clearExistingDecision(gameId: string, playerId: string): void { - for (const [requestId, request] of this.pendingDecisions.entries()) { - if (request.gameId === gameId && request.playerId === playerId) { - clearTimeout(request.timeoutId); - this.pendingDecisions.delete(requestId); - break; - } - } - } - - /** - * Get human-readable description for joker card effect - * @param card The joker card - * @param applied Whether the effect will be applied - * @returns Description string - */ - private getJokerDescription(card: GameCard, applied: boolean): string { - if (!applied) { - return 'No effect applied.'; - } - - if (!card.consequence) { - return 'Apply joker effect!'; - } - - switch (card.consequence.type) { - case 0: // MOVE_FORWARD - return `Move forward ${card.consequence.value || 1} steps!`; - case 1: // MOVE_BACKWARD - return `Move backward ${card.consequence.value || 1} steps!`; - case 2: // LOSE_TURN - return 'Lose your next turn!'; - case 3: // EXTRA_TURN - return 'Get an extra turn!'; - case 5: // GO_TO_START - return 'Go back to start!'; - default: - return 'Apply joker effect!'; - } - } - - /** - * Get remaining time for a pending decision - * @param requestId Request ID - * @returns Remaining time in seconds, or -1 if not found - */ - getRemainingTime(requestId: string): number { - const pending = this.pendingDecisions.get(requestId); - if (!pending) { - return -1; - } - - const elapsed = Date.now() - pending.startTime.getTime(); - const remaining = Math.max(0, this.DECISION_TIMEOUT_MS - elapsed); - return Math.ceil(remaining / 1000); // Return in seconds - } - - /** - * Get count of pending decisions for a game - * @param gameId Game ID - * @returns Number of pending decisions - */ - getPendingDecisionCount(gameId: string): number { - return Array.from(this.pendingDecisions.values()) - .filter(request => request.gameId === gameId).length; - } - - /** - * Determine if a consequence type is negative - * @param consequenceType The consequence type to check - * @returns True if consequence is negative - */ - private isNegativeConsequence(consequenceType?: number): boolean { - if (consequenceType === undefined) return false; - - // Negative consequences: MOVE_BACKWARD, LOSE_TURN, GO_TO_START - return [1, 2, 5].includes(consequenceType); // MOVE_BACKWARD=1, LOSE_TURN=2, GO_TO_START=5 - } - - /** - * Get description for gamemaster decision result - * @param decision Gamemaster's decision - * @param applyConsequence Whether consequence will be applied - * @param card The joker card - * @param isNegative Whether the consequence is negative - * @returns Description string - */ - private getDecisionDescription(decision: GamemasterDecision, applyConsequence: boolean, card: GameCard, isNegative: boolean): string { - if (decision === GamemasterDecision.APPROVE) { - if (isNegative) { - return '🎭 Gamemaster approved - no penalty applied!'; - } else { - return `🎭 Gamemaster approved! ${this.getJokerDescription(card, true)}`; - } - } else { - if (isNegative) { - return `🎭 Gamemaster rejected! ${this.getJokerDescription(card, true)}`; - } else { - return '🎭 Gamemaster rejected - no bonus applied.'; - } - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/Interfaces/GameInterfaces.ts b/SerpentRace_Backend/src/Application/Services/Interfaces/GameInterfaces.ts deleted file mode 100644 index 30a3e640..00000000 --- a/SerpentRace_Backend/src/Application/Services/Interfaces/GameInterfaces.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Shared interfaces for game-related WebSocket communications - * Used by both WebSocketService and GameWebSocketService - */ - -export interface JoinGameRoomData { - gameCode: string; -} - -export interface LeaveGameRoomData { - gameCode: string; -} - -export interface GameStateUpdateData { - gameId: string; - gameCode: string; - players: string[]; - state: string; - currentTurn?: string; -} - -export interface GameActionData { - gameId: string; - gameCode: string; - playerId: string; - action: 'pick_card' | 'play_card' | 'end_turn' | 'leave_game' | 'roll-dice' | 'move' | 'use-field'; - data?: any; -} - -// Field Effect Service WebSocket interfaces -export interface FieldEffectCalculationData { - gameId: string; - gameCode: string; - playerId: string; - currentPosition: number; - card: any; // GameCard - field: any; // GameField - dice: number; - guessedPosition?: number; -} - -export interface FieldEffectResultData { - gameId: string; - gameCode: string; - playerId: string; - result: { - finalPosition: number; - stepValue: number; - dice: number; - patternModifier: number; - consequenceModifier: number; - guessResult?: any; - gamemasterResult?: any; - description: string; - effects: string[]; - turnEffect?: { - type: 'LOSE_TURN' | 'EXTRA_TURN'; - value: number; - playerId: string; - }; - }; -} - -export interface GamemasterDecisionRequestData { - gameId: string; - gameCode: string; - requestId: string; - playerId: string; - playerName: string; - card: any; // GameCard - timeRemaining: number; -} - -export interface GamemasterDecisionResponseData { - requestId: string; - decision: 'approve' | 'reject'; -} - -// Game-specific interfaces for GameWebSocketService -export interface JoinGameData { - gameToken: string; // Required game session token -} - -export interface LeaveGameData { - gameCode: string; -} - -export interface DiceRollData { - gameCode: string; - diceValue: number; // Value from frontend (1-6) -} - -export interface PlayerPosition { - playerId: string; - playerName: string; - boardPosition: number; - turnOrder: number; -} - -export interface GameChatData { - gameCode: string; - message: string; -} - -// Field Effect related types -export interface FieldEffectRequest { - gameId: string; - playerId: string; - playerName: string; - currentPosition: number; - card: any; - field: any; - dice: number; - guessedPosition?: number; -} - -export interface FieldEffectResult { - finalPosition: number; - stepValue: number; - dice: number; - patternModifier: number; - consequenceModifier: number; - guessResult?: any; - gamemasterResult?: any; - description: string; - effects: string[]; - turnEffect?: { - type: 'LOSE_TURN' | 'EXTRA_TURN'; - value: number; - playerId: string; - }; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/JWTService.ts b/SerpentRace_Backend/src/Application/Services/JWTService.ts deleted file mode 100644 index dc2061af..00000000 --- a/SerpentRace_Backend/src/Application/Services/JWTService.ts +++ /dev/null @@ -1,315 +0,0 @@ -import jwt, { SignOptions } from 'jsonwebtoken'; -import { Request, Response } from 'express'; -import { UserState } from '../../Domain/User/UserAggregate'; - -export interface TokenPayload { - userId: string; - authLevel: 0 | 1; - userStatus: UserState; - orgId: string; - type?: 'access'; - iat?: number; - exp?: number; -} - -export interface RefreshTokenPayload { - userId: string; - type: 'refresh'; - orgId?: string; - tokenId?: string; // For token rotation/revocation - iat?: number; - exp?: number; -} - -export interface TokenPair { - accessToken: string; - refreshToken: string; -} - -export class JWTService { - private readonly secretKey: string; - private readonly refreshSecretKey: string; - private readonly tokenExpiry: number; - private readonly refreshTokenExpiry: number; - private readonly cookieName: string; - private readonly refreshCookieName: string; - - constructor() { - this.secretKey = process.env.JWT_SECRET || 'your-secret-key'; - this.refreshSecretKey = process.env.JWT_REFRESH_SECRET || this.secretKey + '_refresh'; - - // Access token expiry (short-lived) - let expiry = 1800; // Default 30 minutes for better security - if (process.env.JWT_EXPIRY) { - expiry = parseInt(process.env.JWT_EXPIRY); - } else if (process.env.JWT_EXPIRATION) { - expiry = this.parseDuration(process.env.JWT_EXPIRATION); - } - - // Refresh token expiry (long-lived) - let refreshExpiry = 604800; // Default 7 days - if (process.env.JWT_REFRESH_EXPIRATION) { - refreshExpiry = this.parseDuration(process.env.JWT_REFRESH_EXPIRATION); - } - - this.tokenExpiry = expiry; - this.refreshTokenExpiry = refreshExpiry; - this.cookieName = 'auth_token'; - this.refreshCookieName = 'refresh_token'; - - if (process.env.NODE_ENV === 'production' && (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'your-secret-key')) { - throw new Error('JWT_SECRET environment variable must be set in production'); - } - } - - /** - * Create a pair of access and refresh tokens - */ - public createTokenPair(payload: Omit): TokenPair { - const now = Math.floor(Date.now() / 1000); - - // Create access token - const accessTokenPayload: TokenPayload = { - ...payload, - type: 'access', - iat: now, - exp: now + this.tokenExpiry - }; - const accessToken = jwt.sign(accessTokenPayload, this.secretKey); - - // Create refresh token - const refreshTokenPayload: RefreshTokenPayload = { - userId: payload.userId, - type: 'refresh', - orgId: payload.orgId, - iat: now, - exp: now + this.refreshTokenExpiry - }; - const refreshToken = jwt.sign(refreshTokenPayload, this.refreshSecretKey); - - return { accessToken, refreshToken }; - } - - /** - * Create access and refresh tokens and set cookies (for cookie-based auth) - */ - create(payload: Omit, res: Response): TokenPair { - const tokenPair = this.createTokenPair(payload); - this.setTokenCookies(res, tokenPair); - return tokenPair; - } - - /** - * Check if the request is using Bearer token authentication - */ - private isUsingBearerAuth(req: Request): boolean { - // No cookie but has Authorization header - return !req.cookies?.[this.cookieName] && - !!req.headers.authorization && - req.headers.authorization.startsWith('Bearer '); - } - - /** - * Verify a refresh token - */ - public verifyRefreshToken(token: string): RefreshTokenPayload | null { - try { - const decoded = jwt.verify(token, this.refreshSecretKey) as RefreshTokenPayload; - if (decoded.type !== 'refresh') { - return null; - } - return decoded; - } catch (error) { - return null; - } - } - - /** - * Attempt to refresh tokens using refresh token from cookies or headers - */ - public attemptTokenRefresh(req: Request, res: Response): TokenPair | null { - try { - // Try to get refresh token from cookie first - let refreshToken = req.cookies[this.refreshCookieName]; - - // If no cookie, try X-Refresh-Token header - if (!refreshToken) { - refreshToken = req.headers['x-refresh-token'] as string; - } - - if (!refreshToken) { - return null; - } - - const refreshPayload = this.verifyRefreshToken(refreshToken); - if (!refreshPayload) { - return null; - } - - // Create new token pair - const newTokenPair = this.createTokenPair({ - userId: refreshPayload.userId, - authLevel: 0, // Default auth level, should be fetched from user data - userStatus: UserState.VERIFIED_REGULAR, // Default status, should be fetched from user data - orgId: refreshPayload.orgId || '' - }); - - // Set new tokens based on authentication method - if (req.cookies[this.cookieName] || req.cookies[this.refreshCookieName]) { - // Cookie-based auth: set new cookies - this.setTokenCookies(res, newTokenPair); - } else { - // Header-based auth: send tokens in response headers - res.setHeader('X-New-Access-Token', newTokenPair.accessToken); - res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken); - res.setHeader('X-Token-Refreshed', 'true'); - } - - return newTokenPair; - } catch (error) { - return null; - } - } - - /** - * Set token cookies for cookie-based authentication - */ - private setTokenCookies(res: Response, tokenPair: TokenPair): void { - // Set access token cookie - res.cookie(this.cookieName, tokenPair.accessToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: this.tokenExpiry * 1000, - }); - - // Set refresh token cookie - res.cookie(this.refreshCookieName, tokenPair.refreshToken, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: this.refreshTokenExpiry * 1000, - }); - } - - verify(req: Request): TokenPayload | null { - try { - // First try to get token from cookie - let token = req.cookies[this.cookieName]; - - // If no cookie token, try Authorization header - if (!token) { - const authHeader = req.headers.authorization; - if (authHeader && authHeader.startsWith('Bearer ')) { - token = authHeader.substring(7); - } - } - - if (!token) return null; - - const decoded = jwt.verify(token, this.secretKey) as TokenPayload; - return decoded; - } catch (error) { - return null; - } - } - - /** - * Logout user by clearing tokens - */ - public logout(req: Request, res: Response): void { - // Clear cookies if they exist - if (req.cookies[this.cookieName]) { - res.clearCookie(this.cookieName, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict' - }); - } - - if (req.cookies[this.refreshCookieName]) { - res.clearCookie(this.refreshCookieName, { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'strict' - }); - } - - // For bearer token auth, set headers to indicate logout - res.setHeader('X-Auth-Logout', 'true'); - res.setHeader('X-Clear-Tokens', 'true'); - } - - // Check if token needs refresh (within 25% of expiry time) - shouldRefreshToken(payload: TokenPayload): boolean { - if (!payload.exp || !payload.iat) return false; - - const now = Math.floor(Date.now() / 1000); - const tokenAge = now - payload.iat; - const tokenLifetime = payload.exp - payload.iat; - const refreshThreshold = tokenLifetime * 0.75; // Refresh when 75% of lifetime has passed - - return tokenAge >= refreshThreshold; - } - - // Conditionally refresh token only if needed - refreshIfNeeded(payload: TokenPayload, res: Response, req?: Request): boolean { - if (this.shouldRefreshToken(payload)) { - if (req) { - // Try to use the new refresh token system - const newTokenPair = this.attemptTokenRefresh(req, res); - if (newTokenPair) { - return true; - } - } - - // Fallback: create new token pair - const freshPayload: Omit = { - userId: payload.userId, - authLevel: payload.authLevel, - userStatus: payload.userStatus, - orgId: payload.orgId - }; - - // Check if using Bearer authentication - if (req && this.isUsingBearerAuth(req)) { - // For Bearer auth, create token pair and add to headers - const newTokenPair = this.createTokenPair(freshPayload); - res.setHeader('X-New-Access-Token', newTokenPair.accessToken); - res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken); - res.setHeader('X-Token-Refreshed', 'true'); - } else { - // For cookie auth, create token pair and set cookies - const newTokenPair = this.create(freshPayload, res); - this.setTokenCookies(res, newTokenPair); - } - - return true; - } - return false; - } - - /** - * Parse duration string to seconds (e.g., "24h", "7d", "30m") - * @param duration Duration string - * @returns Duration in seconds - */ - private parseDuration(duration: string): number { - const match = duration.match(/^(\d+)([smhd])$/); - if (!match) { - throw new Error(`Invalid duration format: ${duration}. Use format like '24h', '7d', '30m'`); - } - - const [, value, unit] = match; - const num = parseInt(value); - - switch (unit) { - case 's': return num; // seconds - case 'm': return num * 60; // minutes - case 'h': return num * 60 * 60; // hours - case 'd': return num * 60 * 60 * 24; // days - default: - throw new Error(`Unsupported duration unit: ${unit}`); - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/Services/Logger.ts b/SerpentRace_Backend/src/Application/Services/Logger.ts deleted file mode 100644 index 74521339..00000000 --- a/SerpentRace_Backend/src/Application/Services/Logger.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { LoggingService, LogLevel } from './LoggingService'; -import { Request, Response } from 'express'; - -// Singleton instance -const logger = LoggingService.getInstance(); - -// Convenience functions for each log level -export const logRequest = (message: string, req?: Request, res?: Response, metadata?: any) => { - logger.log(LogLevel.REQUEST, message, metadata, req, res); -}; - -export const logError = (message: string, error?: Error, req?: Request, res?: Response) => { - const metadata = error ? { - name: error.name, - message: error.message, - stack: error.stack - } : undefined; - logger.log(LogLevel.ERROR, message, metadata, req, res); -}; - -export const logWarning = (message: string, metadata?: any, req?: Request, res?: Response) => { - logger.log(LogLevel.WARNING, message, metadata, req, res); -}; - -export const logAuth = (message: string, userId?: string, metadata?: any, req?: Request, res?: Response) => { - const authMetadata = { - userId, - ...metadata - }; - logger.log(LogLevel.AUTH, message, authMetadata, req, res); -}; - -export const logDatabase = (message: string, query?: string, executionTime?: number, metadata?: any) => { - const dbMetadata = { - query: query ? query.substring(0, 200) : undefined, - executionTime, - ...metadata - }; - logger.log(LogLevel.DATABASE, message, dbMetadata); -}; - -export const logStartup = (message: string, metadata?: any) => { - logger.log(LogLevel.STARTUP, message, metadata); -}; - -export const logConnection = (message: string, type: string, status: 'success' | 'failure' | 'attempt', metadata?: any) => { - const connectionMetadata = { - connectionType: type, - status, - ...metadata - }; - logger.log(LogLevel.CONNECTION, message, connectionMetadata); -}; - -export const logOther = (message: string, metadata?: any, req?: Request, res?: Response) => { - logger.log(LogLevel.OTHER, message, metadata, req, res); -}; - -// Export the main service -export { LoggingService, LogLevel }; -export default logger; diff --git a/SerpentRace_Backend/src/Application/Services/LoggingService.ts b/SerpentRace_Backend/src/Application/Services/LoggingService.ts deleted file mode 100644 index 125ab2a6..00000000 --- a/SerpentRace_Backend/src/Application/Services/LoggingService.ts +++ /dev/null @@ -1,419 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { Request, Response, NextFunction } from 'express'; -import * as Minio from 'minio'; - -export enum LogLevel { - REQUEST = 'REQUEST', - ERROR = 'ERROR', - WARNING = 'WARNING', - AUTH = 'AUTH', - DATABASE = 'DATABASE', - STARTUP = 'STARTUP', - CONNECTION = 'CONNECTION', - OTHER = 'OTHER' -} - -export interface LogEntry { - timestamp: string; - level: LogLevel; - message: string; - metadata?: any; - requestId?: string; - userId?: string; - ip?: string; - userAgent?: string; - method?: string; - url?: string; - statusCode?: number; - responseTime?: number; -} - -export class LoggingService { - private static instance: LoggingService; - private minioClient: Minio.Client | null = null; - private logBuffer: LogEntry[] = []; - private currentLogFile: string | null = null; - private logCount = 0; - private readonly maxLogsPerFile = parseInt(process.env.MAX_LOGS_PER_FILE || '10000'); - private readonly logsDir = path.join(process.cwd(), 'logs'); - private readonly bucketName = process.env.MINIO_BUCKET_NAME || 'serpentrace-logs'; - private uploadInterval: NodeJS.Timeout | null = null; - - private constructor() { - this.initializeLogsDirectory(); - this.initializeMinioClient(); - this.createNewLogFile(); - - if (process.env.NODE_ENV !== 'test') { - this.startPeriodicUpload(); - } - - process.on('SIGTERM', () => this.shutdown()); - process.on('SIGINT', () => this.shutdown()); - process.on('beforeExit', () => this.shutdown()); - } - - static getInstance(): LoggingService { - if (!LoggingService.instance) { - LoggingService.instance = new LoggingService(); - } - return LoggingService.instance; - } - - private initializeLogsDirectory(): void { - try { - if (!fs.existsSync(this.logsDir)) { - fs.mkdirSync(this.logsDir, { recursive: true }); - } - - // Create monthly subdirectory - const monthlyDir = this.getMonthlyDirectory(); - if (!fs.existsSync(monthlyDir)) { - fs.mkdirSync(monthlyDir, { recursive: true }); - } - } catch (error) { - console.error('Failed to initialize logs directory:', error); - } - } - - private initializeMinioClient(): void { - try { - // Check if in production or development - if (process.env.NODE_ENV === 'production') { - if (process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY) { - this.minioClient = new Minio.Client({ - endPoint: process.env.MINIO_ENDPOINT, - port: parseInt(process.env.MINIO_PORT || '9000'), - useSSL: process.env.MINIO_USE_SSL === 'true', - accessKey: process.env.MINIO_ACCESS_KEY, - secretKey: process.env.MINIO_SECRET_KEY - }); - - this.ensureBucketExists(); - } else { - console.warn('Minio configuration not found. Logs will only be stored locally and in console.'); - } - } else { - // Development mode - only use MinIO if explicitly configured - if (process.env.MINIO_ENDPOINT || process.env.ENABLE_MINIO === 'true') { - this.minioClient = new Minio.Client({ - endPoint: process.env.MINIO_ENDPOINT || 'localhost', - port: parseInt(process.env.MINIO_PORT || '9000'), - useSSL: false, - accessKey: process.env.MINIO_ACCESS_KEY || 'serpentrace', - secretKey: process.env.MINIO_SECRET_KEY || 'serpentrace123!' - }); - - this.ensureBucketExists(); - } else { - console.log('Development mode: MinIO disabled. Set ENABLE_MINIO=true to enable MinIO logging.'); - this.minioClient = null; - } - } - - - } catch (error) { - console.error('Failed to initialize Minio client:', error); - this.minioClient = null; - } - } - - private async ensureBucketExists(): Promise { - if (!this.minioClient) return; - - try { - const exists = await this.minioClient.bucketExists(this.bucketName); - if (!exists) { - await this.minioClient.makeBucket(this.bucketName); - this.log(LogLevel.STARTUP, `Created Minio bucket: ${this.bucketName}`); - } - } catch (error) { - console.warn('MinIO connection failed - disabling MinIO logging:', (error as Error).message); - // Disable MinIO client if connection fails - this.minioClient = null; - } - } - - private startPeriodicUpload(): void { - // Upload current log file to Minio every 2 minutes - this.uploadInterval = setInterval(async () => { - if (this.currentLogFile && this.minioClient) { - await this.uploadToMinio(this.currentLogFile); - } - }, 2 * 60 * 1000); // 2 minutes - } - - private getMonthlyDirectory(): string { - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - return path.join(this.logsDir, `${year}-${month}`); - } - - private getMonthlyMinioPrefix(): string { - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - return `${year}-${month}/`; - } - - private createNewLogFile(): void { - const now = new Date(); - const timestamp = now.toISOString().replace(/[:.]/g, '-'); - const fileName = `serpentrace-${timestamp}.log`; - - this.currentLogFile = path.join(this.getMonthlyDirectory(), fileName); - this.logCount = 0; - - // Write log file header - const header = `# SerpentRace Backend Logs\n# Started: ${now.toISOString()}\n# Max entries per file: ${this.maxLogsPerFile}\n\n`; - try { - fs.writeFileSync(this.currentLogFile, header); - } catch (error) { - console.error('Failed to create log file:', error); - } - } - - private formatLogEntry(entry: LogEntry): string { - const parts = [ - entry.timestamp, - `[${entry.level}]`, - entry.message - ]; - - if (entry.requestId) parts.push(`ReqId:${entry.requestId}`); - if (entry.userId) parts.push(`UserId:${entry.userId}`); - if (entry.ip) parts.push(`IP:${entry.ip}`); - if (entry.method && entry.url) parts.push(`${entry.method} ${entry.url}`); - if (entry.statusCode) parts.push(`Status:${entry.statusCode}`); - if (entry.responseTime) parts.push(`Time:${entry.responseTime}ms`); - if (entry.userAgent) parts.push(`UA:${entry.userAgent.substring(0, 50)}`); - if (entry.metadata) parts.push(`Meta:${JSON.stringify(entry.metadata)}`); - - return parts.join(' | '); - } - - private async writeToLocalFile(entry: LogEntry): Promise { - if (!this.currentLogFile) return; - - try { - const logLine = this.formatLogEntry(entry) + '\n'; - fs.appendFileSync(this.currentLogFile, logLine); - - this.logCount++; - - // Check if we need to rotate the log file - if (this.logCount >= this.maxLogsPerFile) { - await this.rotateLogFile(); - } - } catch (error) { - console.error('Failed to write to log file:', error); - } - } - - private async rotateLogFile(): Promise { - if (!this.currentLogFile) return; - - try { - // Upload current file to Minio before rotating - await this.uploadToMinio(this.currentLogFile); - - // Create new log file - this.createNewLogFile(); - - this.log(LogLevel.OTHER, 'Log file rotated due to size limit'); - } catch (error) { - console.error('Failed to rotate log file:', error); - } - } - - private async uploadToMinio(filePath: string): Promise { - if (!this.minioClient) { - console.warn('Minio client not initialized, skipping upload'); - return; - } - - if (!fs.existsSync(filePath)) { - console.warn(`Log file does not exist: ${filePath}`); - return; - } - - try { - const fileName = path.basename(filePath); - const objectName = this.getMonthlyMinioPrefix() + fileName; - - console.log(`Attempting to upload log file to Minio: ${objectName}`); - await this.minioClient.fPutObject(this.bucketName, objectName, filePath); - console.log(`Successfully uploaded log file to Minio: ${objectName}`); - } catch (error) { - console.error('Failed to upload to Minio:', error); - console.error('Minio config:', { - endpoint: this.minioClient ? 'configured' : 'not configured', - bucket: this.bucketName - }); - } - } - - private logToConsole(entry: LogEntry): void { - // In production, skip OTHER, CONNECTION, and REQUEST logs - if (process.env.NODE_ENV === 'production') { - if (entry.level === LogLevel.OTHER || - entry.level === LogLevel.CONNECTION || - entry.level === LogLevel.REQUEST) { - return; - } - } - - const formattedEntry = this.formatLogEntry(entry); - - switch (entry.level) { - case LogLevel.ERROR: - console.error(formattedEntry); - break; - case LogLevel.WARNING: - console.warn(formattedEntry); - break; - case LogLevel.REQUEST: - case LogLevel.AUTH: - case LogLevel.DATABASE: - case LogLevel.CONNECTION: - console.info(formattedEntry); - break; - case LogLevel.STARTUP: - console.log(formattedEntry); - break; - default: - console.log(formattedEntry); - } - } - - public log( - level: LogLevel, - message: string, - metadata?: any, - req?: Request, - res?: Response, - responseTime?: number - ): void { - // In production, skip OTHER, CONNECTION, and REQUEST logs entirely - if (process.env.NODE_ENV === 'production') { - if (level === LogLevel.OTHER || - level === LogLevel.CONNECTION || - level === LogLevel.REQUEST) { - return; - } - } - - const entry: LogEntry = { - timestamp: new Date().toISOString(), - level, - message, - metadata - }; - - // Add request context if available - if (req) { - entry.requestId = (req as any).requestId || this.generateRequestId(); - entry.userId = (req as any).user?.userId; - entry.ip = req.ip || req.socket?.remoteAddress || 'unknown'; - entry.userAgent = req.get ? req.get('User-Agent') : 'unknown'; - entry.method = req.method; - entry.url = req.originalUrl || req.url; - } - - if (res) { - entry.statusCode = res.statusCode; - } - - if (responseTime !== undefined) { - entry.responseTime = responseTime; - } - - // Log to all three destinations - this.logToConsole(entry); - this.writeToLocalFile(entry); - - // Add to buffer for potential batch processing - this.logBuffer.push(entry); - - // Limit buffer size - if (this.logBuffer.length > 1000) { - this.logBuffer = this.logBuffer.slice(-500); - } - } - - private generateRequestId(): string { - return Math.random().toString(36).substr(2, 9); - } - - public async shutdown(): Promise { - try { - // Clear the upload interval - if (this.uploadInterval) { - clearInterval(this.uploadInterval); - this.uploadInterval = null; - } - - // Upload current log file to Minio - if (this.currentLogFile) { - await this.uploadToMinio(this.currentLogFile); - } - - this.log(LogLevel.STARTUP, 'Logging service shutting down gracefully'); - - // Give time for final logs to be written - await new Promise(resolve => setTimeout(resolve, 1000)); - } catch (error) { - console.error('Error during logging service shutdown:', error); - } - } - - // Middleware factory methods - public requestLoggingMiddleware() { - return (req: Request, res: Response, next: NextFunction) => { - const startTime = Date.now(); - - // Generate request ID - (req as any).requestId = this.generateRequestId(); - - // Log request start - this.log(LogLevel.REQUEST, `Incoming request`, undefined, req); - - // Override res.end to log response - const originalEnd = res.end.bind(res); - res.end = (...args: any[]): Response => { - const responseTime = Date.now() - startTime; - LoggingService.getInstance().log( - LogLevel.REQUEST, - `Request completed`, - undefined, - req, - res, - responseTime - ); - return originalEnd(...args); - }; - - next(); - }; - } - - public errorLoggingMiddleware() { - return (error: Error, req: Request, res: Response, next: NextFunction) => { - this.log( - LogLevel.ERROR, - `Unhandled error: ${error.message}`, - { - stack: error.stack, - name: error.name - }, - req, - res - ); - next(error); - }; - } -} - -export default LoggingService; diff --git a/SerpentRace_Backend/src/Application/Services/PasswordService.ts b/SerpentRace_Backend/src/Application/Services/PasswordService.ts deleted file mode 100644 index 56f3ec08..00000000 --- a/SerpentRace_Backend/src/Application/Services/PasswordService.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as bcrypt from 'bcrypt'; -import { logError } from './Logger'; - -export class PasswordService { - private static readonly SALT_ROUNDS = 12; - - /** - * Hashes a plain text password using bcrypt - * @param password - The plain text password to hash - * @returns Promise - The hashed password - */ - static async hashPassword(password: string): Promise { - try { - if (!password || typeof password !== 'string') { - throw new Error('Password must be a non-empty string'); - } - - return await bcrypt.hash(password, this.SALT_ROUNDS); - } catch (error) { - logError('PasswordService.hashPassword error', error instanceof Error ? error : new Error(String(error))); - - if (error instanceof Error && error.message === 'Password must be a non-empty string') { - throw error; // Re-throw validation errors as-is - } - - throw new Error('Failed to hash password'); - } - } - - /** - * Verifies a plain text password against a hashed password - * @param password - The plain text password to verify - * @param hashedPassword - The hashed password to compare against - * @returns Promise - True if password matches, false otherwise - */ - static async verifyPassword(password: string, hashedPassword: string): Promise { - try { - if (!password || typeof password !== 'string') { - return false; // Invalid input should return false, not throw - } - - if (!hashedPassword || typeof hashedPassword !== 'string') { - return false; // Invalid input should return false, not throw - } - - return await bcrypt.compare(password, hashedPassword); - } catch (error) { - logError('PasswordService.verifyPassword error', error instanceof Error ? error : new Error(String(error))); - return false; // Return false on error instead of throwing - } - } - - /** - * Validates password strength requirements - * @param password - The password to validate - * @returns object - Object containing isValid boolean and error messages - */ - static validatePasswordStrength(password: string): { isValid: boolean; errors: string[] } { - try { - const errors: string[] = []; - - if (!password || typeof password !== 'string') { - errors.push('Password must be provided as a string'); - return { isValid: false, errors }; - } - - if (password.length < 8) { - errors.push('Password must be at least 8 characters long'); - } - - if (!/[A-Z]/.test(password)) { - errors.push('Password must contain at least one uppercase letter'); - } - - if (!/[a-z]/.test(password)) { - errors.push('Password must contain at least one lowercase letter'); - } - - if (!/\d/.test(password)) { - errors.push('Password must contain at least one number'); - } - - if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { - errors.push('Password must contain at least one special character'); - } - - return { - isValid: errors.length === 0, - errors - }; - } catch (error) { - logError('PasswordService.validatePasswordStrength error', error instanceof Error ? error : new Error(String(error))); - return { - isValid: false, - errors: ['Password validation failed due to internal error'] - }; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Services/RedisService.ts b/SerpentRace_Backend/src/Application/Services/RedisService.ts deleted file mode 100644 index de0b38bb..00000000 --- a/SerpentRace_Backend/src/Application/Services/RedisService.ts +++ /dev/null @@ -1,423 +0,0 @@ -import { createClient, RedisClientType } from 'redis'; -import { logError, logStartup, logWarning } from './Logger'; - -export interface ActiveChatData { - chatId: string; - participants: string[]; - lastActivity: Date; - messageCount: number; - chatType: 'direct' | 'group' | 'game'; - gameId?: string; - name?: string; -} - -export interface ActiveUserData { - userId: string; - activeChatIds: string[]; - lastActivity: Date; - isOnline: boolean; -} - -export class RedisService { - private static instance: RedisService; - private client: RedisClientType; - private isConnected: boolean = false; - - private constructor() { - const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; - this.client = createClient({ - url: redisUrl, - socket: { - reconnectStrategy: (retries) => Math.min(retries * 50, 500) - } - }); - - this.client.on('error', (err) => { - logError('Redis connection error', err); - this.isConnected = false; - }); - - this.client.on('connect', () => { - logStartup('Redis client connected successfully'); - this.isConnected = true; - }); - - this.client.on('disconnect', () => { - logWarning('Redis client disconnected'); - this.isConnected = false; - }); - } - - public static getInstance(): RedisService { - if (!RedisService.instance) { - RedisService.instance = new RedisService(); - } - return RedisService.instance; - } - - public async connect(): Promise { - try { - if (!this.isConnected) { - await this.client.connect(); - } - } catch (error) { - logError('Failed to connect to Redis', error as Error); - throw error; - } - } - - public async disconnect(): Promise { - try { - if (this.isConnected) { - await this.client.disconnect(); - } - } catch (error) { - logError('Failed to disconnect from Redis', error as Error); - } - } - - public async setActiveChat(chatId: string, chatData: ActiveChatData): Promise { - try { - const key = `active_chat:${chatId}`; - await this.client.hSet(key, { - chatId: chatData.chatId, - participants: JSON.stringify(chatData.participants), - lastActivity: chatData.lastActivity.toISOString(), - messageCount: chatData.messageCount.toString(), - chatType: chatData.chatType, - gameId: chatData.gameId || '', - name: chatData.name || '' - }); - - // Set expiration for 1 hour of inactivity - await this.client.expire(key, 3600); - } catch (error) { - logError(`Failed to set active chat ${chatId}`, error as Error); - } - } - - public async getActiveChat(chatId: string): Promise { - try { - const key = `active_chat:${chatId}`; - const data = await this.client.hGetAll(key); - - if (!data.chatId) { - return null; - } - - return { - chatId: data.chatId, - participants: JSON.parse(data.participants), - lastActivity: new Date(data.lastActivity), - messageCount: parseInt(data.messageCount, 10), - chatType: data.chatType as 'direct' | 'group' | 'game', - gameId: data.gameId || undefined, - name: data.name || undefined - }; - } catch (error) { - logError(`Failed to get active chat ${chatId}`, error as Error); - return null; - } - } - - public async removeActiveChat(chatId: string): Promise { - try { - const key = `active_chat:${chatId}`; - await this.client.del(key); - } catch (error) { - logError(`Failed to remove active chat ${chatId}`, error as Error); - } - } - - public async getAllActiveChats(): Promise { - try { - const pattern = 'active_chat:*'; - const keys = await this.client.keys(pattern); - const chats: ActiveChatData[] = []; - - for (const key of keys) { - const data = await this.client.hGetAll(key); - if (data.chatId) { - chats.push({ - chatId: data.chatId, - participants: JSON.parse(data.participants), - lastActivity: new Date(data.lastActivity), - messageCount: parseInt(data.messageCount, 10), - chatType: data.chatType as 'direct' | 'group' | 'game', - gameId: data.gameId || undefined, - name: data.name || undefined - }); - } - } - - return chats; - } catch (error) { - logError('Failed to get all active chats', error as Error); - return []; - } - } - - public async setActiveUser(userId: string, userData: ActiveUserData): Promise { - try { - const key = `active_user:${userId}`; - await this.client.hSet(key, { - userId: userData.userId, - activeChatIds: JSON.stringify(userData.activeChatIds), - lastActivity: userData.lastActivity.toISOString(), - isOnline: userData.isOnline.toString() - }); - - // Set expiration for 2 hours - await this.client.expire(key, 7200); - } catch (error) { - logError(`Failed to set active user ${userId}`, error as Error); - } - } - - public async getActiveUser(userId: string): Promise { - try { - const key = `active_user:${userId}`; - const data = await this.client.hGetAll(key); - - if (!data.userId) { - return null; - } - - return { - userId: data.userId, - activeChatIds: JSON.parse(data.activeChatIds), - lastActivity: new Date(data.lastActivity), - isOnline: data.isOnline === 'true' - }; - } catch (error) { - logError(`Failed to get active user ${userId}`, error as Error); - return null; - } - } - - public async removeActiveUser(userId: string): Promise { - try { - const key = `active_user:${userId}`; - await this.client.del(key); - } catch (error) { - logError(`Failed to remove active user ${userId}`, error as Error); - } - } - - public async addUserToChat(userId: string, chatId: string): Promise { - try { - const userData = await this.getActiveUser(userId) || { - userId, - activeChatIds: [], - lastActivity: new Date(), - isOnline: true - }; - - if (!userData.activeChatIds.includes(chatId)) { - userData.activeChatIds.push(chatId); - userData.lastActivity = new Date(); - await this.setActiveUser(userId, userData); - } - } catch (error) { - logError(`Failed to add user ${userId} to chat ${chatId}`, error as Error); - } - } - - public async removeUserFromChat(userId: string, chatId: string): Promise { - try { - const userData = await this.getActiveUser(userId); - if (userData) { - userData.activeChatIds = userData.activeChatIds.filter(id => id !== chatId); - userData.lastActivity = new Date(); - await this.setActiveUser(userId, userData); - } - } catch (error) { - logError(`Failed to remove user ${userId} from chat ${chatId}`, error as Error); - } - } - - public async getUserActiveChats(userId: string): Promise { - try { - const userData = await this.getActiveUser(userId); - return userData?.activeChatIds || []; - } catch (error) { - logError(`Failed to get active chats for user ${userId}`, error as Error); - return []; - } - } - - public async updateChatActivity(chatId: string, messageCount?: number): Promise { - try { - const chatData = await this.getActiveChat(chatId); - if (chatData) { - chatData.lastActivity = new Date(); - if (messageCount !== undefined) { - chatData.messageCount = messageCount; - } - await this.setActiveChat(chatId, chatData); - } - } catch (error) { - logError(`Failed to update chat activity ${chatId}`, error as Error); - } - } - - public async getInactiveChats(inactivityMinutes: number): Promise { - try { - const cutoffTime = new Date(Date.now() - inactivityMinutes * 60 * 1000); - const allChats = await this.getAllActiveChats(); - - return allChats - .filter(chat => chat.lastActivity < cutoffTime) - .map(chat => chat.chatId); - } catch (error) { - logError('Failed to get inactive chats', error as Error); - return []; - } - } - - public async cleanupInactiveChats(inactivityMinutes: number): Promise { - try { - const inactiveChats = await this.getInactiveChats(inactivityMinutes); - - for (const chatId of inactiveChats) { - await this.removeActiveChat(chatId); - } - - return inactiveChats; - } catch (error) { - logError('Failed to cleanup inactive chats', error as Error); - return []; - } - } - - public async ping(): Promise { - try { - const result = await this.client.ping(); - return result === 'PONG'; - } catch (error) { - logError('Redis ping failed', error as Error); - return false; - } - } - - public isRedisConnected(): boolean { - return this.isConnected; - } - - // Generic Redis methods for game data - public async get(key: string): Promise { - try { - const value = await this.client.get(key); - // Refresh TTL on access for game-related keys - if (value && this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - return value; - } catch (error) { - logError(`Failed to get key ${key}`, error as Error); - return null; - } - } - - public async set(key: string, value: string): Promise { - try { - await this.client.set(key, value); - // Auto-expire game-related keys after 30 minutes - if (this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // 30 minutes - } - } catch (error) { - logError(`Failed to set key ${key}`, error as Error); - } - } - - public async setWithExpiry(key: string, value: string, expirySeconds: number): Promise { - try { - await this.client.setEx(key, expirySeconds, value); - } catch (error) { - logError(`Failed to set key ${key} with expiry`, error as Error); - } - } - - public async del(key: string): Promise { - try { - await this.client.del(key); - } catch (error) { - logError(`Failed to delete key ${key}`, error as Error); - } - } - - public async setAdd(key: string, member: string): Promise { - try { - await this.client.sAdd(key, member); - // Refresh TTL for game-related keys - if (this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - } catch (error) { - logError(`Failed to add member to set ${key}`, error as Error); - } - } - - public async setRemove(key: string, member: string): Promise { - try { - await this.client.sRem(key, member); - // Refresh TTL for game-related keys - if (this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - } catch (error) { - logError(`Failed to remove member from set ${key}`, error as Error); - } - } - - public async setMembers(key: string): Promise { - try { - const members = await this.client.sMembers(key); - // Refresh TTL on access for game-related keys - if (members.length > 0 && this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - return members; - } catch (error) { - logError(`Failed to get members of set ${key}`, error as Error); - return []; - } - } - - public async exists(key: string): Promise { - try { - const result = await this.client.exists(key); - // Refresh TTL on access for game-related keys - if (result === 1 && this.isGameRelatedKey(key)) { - await this.client.expire(key, 1800); // Reset to 30 minutes - } - return result === 1; - } catch (error) { - logError(`Failed to check existence of key ${key}`, error as Error); - return false; - } - } - - /** - * Check if a key is game-related and should have auto-expiration - * Game-related patterns: gameplay:*, game:*, game_*, board:*, game_pending_card:*, etc. - */ - private isGameRelatedKey(key: string): boolean { - const gamePatterns = [ - 'gameplay:', - 'game:', - 'game_', - 'board:', - 'game_pending_card:', - 'game_pending_decision:', - 'game_player_extra_turns:', - 'game_player_turns_to_lose:', - 'game_positions:', - 'game_ready:', - 'game_room:', - 'active_game:' - ]; - return gamePatterns.some(pattern => key.startsWith(pattern)); - } -} diff --git a/SerpentRace_Backend/src/Application/Services/TokenService.ts b/SerpentRace_Backend/src/Application/Services/TokenService.ts deleted file mode 100644 index 48ff12fd..00000000 --- a/SerpentRace_Backend/src/Application/Services/TokenService.ts +++ /dev/null @@ -1,229 +0,0 @@ -import * as crypto from 'crypto'; -import { logError } from './Logger'; - -export interface VerificationToken { - token: string; - expiresAt: Date; - createdAt: Date; -} - -export interface PasswordResetToken { - token: string; - expiresAt: Date; - createdAt: Date; -} - -export class TokenService { - private static readonly VERIFICATION_TOKEN_EXPIRES_HOURS = 24; - private static readonly PASSWORD_RESET_TOKEN_EXPIRES_HOURS = 1; - private static readonly TOKEN_LENGTH = 32; - - /** - * Generate a secure random token - * @param length - Length of the token in bytes (default: 32) - * @returns Hexadecimal string token - */ - static generateSecureToken(length: number = TokenService.TOKEN_LENGTH): string { - try { - return crypto.randomBytes(length).toString('hex'); - } catch (error) { - logError('TokenService.generateSecureToken error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate secure token'); - } - } - - /** - * Generate email verification token with expiration - * @returns VerificationToken object with token and expiration info - */ - static generateVerificationToken(): VerificationToken { - try { - const token = this.generateSecureToken(); - const createdAt = new Date(); - const expiresAt = new Date(createdAt.getTime() + (this.VERIFICATION_TOKEN_EXPIRES_HOURS * 60 * 60 * 1000)); - - return { - token, - createdAt, - expiresAt - }; - } catch (error) { - logError('TokenService.generateVerificationToken error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate verification token'); - } - } - - /** - * Generate password reset token with expiration - * @returns PasswordResetToken object with token and expiration info - */ - static generatePasswordResetToken(): PasswordResetToken { - try { - const token = this.generateSecureToken(); - const createdAt = new Date(); - const expiresAt = new Date(createdAt.getTime() + (this.PASSWORD_RESET_TOKEN_EXPIRES_HOURS * 60 * 60 * 1000)); - - return { - token, - createdAt, - expiresAt - }; - } catch (error) { - logError('TokenService.generatePasswordResetToken error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate password reset token'); - } - } - - /** - * Check if a token has expired - * @param expiresAt - Expiration date of the token - * @returns True if token has expired, false otherwise - */ - static isTokenExpired(expiresAt: Date): boolean { - try { - return new Date() > expiresAt; - } catch (error) { - logError('TokenService.isTokenExpired error', error instanceof Error ? error : new Error(String(error))); - return true; // Assume expired on error for security - } - } - - /** - * Validate token format (basic validation) - * @param token - Token to validate - * @returns True if token format is valid, false otherwise - */ - static isValidTokenFormat(token: string): boolean { - try { - if (!token || typeof token !== 'string') { - return false; - } - - // Check if token is hexadecimal and has expected length - const hexRegex = /^[a-f0-9]+$/i; - const expectedLength = this.TOKEN_LENGTH * 2; // Each byte becomes 2 hex characters - - return hexRegex.test(token) && token.length === expectedLength; - } catch (error) { - logError('TokenService.isValidTokenFormat error', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Generate a verification URL with token - * @param baseUrl - Base URL of the application - * @param token - Verification token - * @returns Complete verification URL - */ - static generateVerificationUrl(baseUrl: string, token: string): string { - try { - // Remove trailing slash from baseUrl if present - const cleanBaseUrl = baseUrl.replace(/\/$/, ''); - return `${cleanBaseUrl}/verify-email?token=${encodeURIComponent(token)}`; - } catch (error) { - logError('TokenService.generateVerificationUrl error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate verification URL'); - } - } - - /** - * Generate a password reset URL with token - * @param baseUrl - Base URL of the application - * @param token - Password reset token - * @returns Complete password reset URL - */ - static generatePasswordResetUrl(baseUrl: string, token: string): string { - try { - // Remove trailing slash from baseUrl if present - const cleanBaseUrl = baseUrl.replace(/\/$/, ''); - return `${cleanBaseUrl}/reset-password?token=${encodeURIComponent(token)}`; - } catch (error) { - logError('TokenService.generatePasswordResetUrl error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to generate password reset URL'); - } - } - - /** - * Hash a token for secure storage in database - * @param token - Plain text token to hash - * @returns Hashed token - */ - static async hashToken(token: string): Promise { - try { - if (!token || typeof token !== 'string') { - throw new Error('Token must be a non-empty string'); - } - - return crypto.createHash('sha256').update(token).digest('hex'); - } catch (error) { - logError('TokenService.hashToken error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to hash token'); - } - } - - /** - * Verify a plain text token against a hashed token - * @param plainToken - Plain text token to verify - * @param hashedToken - Hashed token to compare against - * @returns True if tokens match, false otherwise - */ - static async verifyToken(plainToken: string, hashedToken: string): Promise { - try { - if (!plainToken || !hashedToken) { - return false; - } - - const hashedPlainToken = await this.hashToken(plainToken); - return hashedPlainToken === hashedToken; - } catch (error) { - logError('TokenService.verifyToken error', error instanceof Error ? error : new Error(String(error))); - return false; - } - } - - /** - * Get token expiration info in human-readable format - * @param expiresAt - Expiration date - * @returns Human-readable expiration info - */ - static getExpirationInfo(expiresAt: Date): { expired: boolean; timeLeft: string } { - try { - const now = new Date(); - const expired = now > expiresAt; - - if (expired) { - const timeAgo = Math.floor((now.getTime() - expiresAt.getTime()) / (1000 * 60)); - return { - expired: true, - timeLeft: `Expired ${timeAgo} minute(s) ago` - }; - } - - const timeLeft = Math.floor((expiresAt.getTime() - now.getTime()) / (1000 * 60)); - const hours = Math.floor(timeLeft / 60); - const minutes = timeLeft % 60; - - let timeString = ''; - if (hours > 0) { - timeString = `${hours} hour(s)`; - if (minutes > 0) { - timeString += ` and ${minutes} minute(s)`; - } - } else { - timeString = `${minutes} minute(s)`; - } - - return { - expired: false, - timeLeft: `Expires in ${timeString}` - }; - } catch (error) { - logError('TokenService.getExpirationInfo error', error instanceof Error ? error : new Error(String(error))); - return { - expired: true, - timeLeft: 'Unable to determine expiration' - }; - } - } -} diff --git a/SerpentRace_Backend/src/Application/Services/TurnHistoryService.ts b/SerpentRace_Backend/src/Application/Services/TurnHistoryService.ts deleted file mode 100644 index 28d332fa..00000000 --- a/SerpentRace_Backend/src/Application/Services/TurnHistoryService.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ITurnHistoryRepository } from '../../Domain/IRepository/ITurnHistoryRepository'; -import { TurnHistoryAggregate, TurnActionType, TurnActionData } from '../../Domain/Game/TurnHistoryAggregate'; -import { logOther, logError } from './Logger'; - -export class TurnHistoryService { - constructor(private turnHistoryRepository: ITurnHistoryRepository) {} - - /** - * Log a turn action - */ - async logTurnAction( - gameId: string, - playerId: string, - playerName: string, - turnNumber: number, - actionType: TurnActionType, - positionBefore: number, - positionAfter: number, - actionData?: TurnActionData - ): Promise { - try { - const turnHistory = new TurnHistoryAggregate(); - turnHistory.gameid = gameId; - turnHistory.playerid = playerId; - turnHistory.playername = playerName; - turnHistory.turnNumber = turnNumber; - turnHistory.actionType = actionType; - turnHistory.positionBefore = positionBefore; - turnHistory.positionAfter = positionAfter; - turnHistory.actionData = actionData || null; - - await this.turnHistoryRepository.save(turnHistory); - - logOther(`Turn history logged: ${actionType}`, { - gameId, - playerId, - playerName, - turnNumber, - positionBefore, - positionAfter - }); - } catch (error) { - logError('Failed to log turn history', error as Error); - // Don't throw - logging shouldn't break game flow - } - } - - /** - * Get game replay data - */ - async getGameReplay(gameId: string): Promise { - return await this.turnHistoryRepository.findByGameId(gameId); - } - - /** - * Get player's turn history in a game - */ - async getPlayerHistory(gameId: string, playerId: string): Promise { - return await this.turnHistoryRepository.findByGameAndPlayer(gameId, playerId); - } - - /** - * Get recent turns for a game - */ - async getRecentTurns(gameId: string, limit: number = 10): Promise { - return await this.turnHistoryRepository.findLastNTurns(gameId, limit); - } - - /** - * Clean up turn history for a finished game - */ - async cleanupGameHistory(gameId: string): Promise { - try { - await this.turnHistoryRepository.deleteByGameId(gameId); - logOther(`Turn history cleaned up for game ${gameId}`); - } catch (error) { - logError('Failed to cleanup turn history', error as Error); - } - } -} diff --git a/SerpentRace_Backend/src/Application/Services/ValidationMiddleware.ts b/SerpentRace_Backend/src/Application/Services/ValidationMiddleware.ts deleted file mode 100644 index cabe01b9..00000000 --- a/SerpentRace_Backend/src/Application/Services/ValidationMiddleware.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { ErrorResponseService } from './ErrorResponseService'; -import { logError, logWarning } from './Logger'; - -/** - * Common validation middleware functions for request validation - */ -export class ValidationMiddleware { - - /** - * Validates required fields in request body - * @param requiredFields Array of required field names - */ - static validateRequiredFields(requiredFields: string[]) { - return (req: Request, res: Response, next: NextFunction) => { - const missingFields: string[] = []; - - for (const field of requiredFields) { - if (!req.body || req.body[field] === undefined || req.body[field] === null || req.body[field] === '') { - missingFields.push(field); - } - } - - if (missingFields.length > 0) { - logWarning('Validation failed - missing required fields', { - missingFields, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Missing required fields', - { missingFields } - ); - } - - next(); - }; - } - - /** - * Validates field types in request body - * @param fieldTypes Object mapping field names to expected types - */ - static validateFieldTypes(fieldTypes: Record) { - return (req: Request, res: Response, next: NextFunction) => { - const typeErrors: string[] = []; - - for (const [field, expectedType] of Object.entries(fieldTypes)) { - if (req.body && req.body[field] !== undefined) { - const actualType = Array.isArray(req.body[field]) ? 'array' : typeof req.body[field]; - - if (actualType !== expectedType) { - typeErrors.push(`Field '${field}' should be ${expectedType}, got ${actualType}`); - } - } - } - - if (typeErrors.length > 0) { - logWarning('Validation failed - invalid field types', { - typeErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Invalid field types', - { errors: typeErrors } - ); - } - - next(); - }; - } - - /** - * Validates string field length constraints - * @param constraints Object mapping field names to min/max length - */ - static validateStringLength(constraints: Record) { - return (req: Request, res: Response, next: NextFunction) => { - const lengthErrors: string[] = []; - - for (const [field, constraint] of Object.entries(constraints)) { - if (req.body && typeof req.body[field] === 'string') { - const value = req.body[field]; - - if (constraint.min !== undefined && value.length < constraint.min) { - lengthErrors.push(`Field '${field}' must be at least ${constraint.min} characters`); - } - - if (constraint.max !== undefined && value.length > constraint.max) { - lengthErrors.push(`Field '${field}' must not exceed ${constraint.max} characters`); - } - } - } - - if (lengthErrors.length > 0) { - logWarning('Validation failed - string length constraints', { - lengthErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'String length validation failed', - { errors: lengthErrors } - ); - } - - next(); - }; - } - - /** - * Validates email format - * @param emailFields Array of field names that should contain valid emails - */ - static validateEmailFormat(emailFields: string[]) { - return (req: Request, res: Response, next: NextFunction) => { - const emailErrors: string[] = []; - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - for (const field of emailFields) { - if (req.body && req.body[field] && typeof req.body[field] === 'string') { - if (!emailRegex.test(req.body[field])) { - emailErrors.push(`Field '${field}' must contain a valid email address`); - } - } - } - - if (emailErrors.length > 0) { - logWarning('Validation failed - invalid email format', { - emailErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Email format validation failed', - { errors: emailErrors } - ); - } - - next(); - }; - } - - /** - * Validates UUIDs format - * @param uuidFields Array of field names that should contain valid UUIDs - */ - static validateUUIDFormat(uuidFields: string[]) { - return (req: Request, res: Response, next: NextFunction) => { - const uuidErrors: string[] = []; - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - - for (const field of uuidFields) { - const value = field.includes('.') - ? this.getNestedValue(req, field) - : req.body?.[field] || req.params?.[field] || req.query?.[field]; - - if (value && typeof value === 'string') { - if (!uuidRegex.test(value)) { - uuidErrors.push(`Field '${field}' must contain a valid UUID`); - } - } - } - - if (uuidErrors.length > 0) { - logWarning('Validation failed - invalid UUID format', { - uuidErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'UUID format validation failed', - { errors: uuidErrors } - ); - } - - next(); - }; - } - - /** - * Validates numeric constraints - * @param constraints Object mapping field names to min/max values - */ - static validateNumericConstraints(constraints: Record) { - return (req: Request, res: Response, next: NextFunction) => { - const numericErrors: string[] = []; - - for (const [field, constraint] of Object.entries(constraints)) { - if (req.body && typeof req.body[field] === 'number') { - const value = req.body[field]; - - if (constraint.min !== undefined && value < constraint.min) { - numericErrors.push(`Field '${field}' must be at least ${constraint.min}`); - } - - if (constraint.max !== undefined && value > constraint.max) { - numericErrors.push(`Field '${field}' must not exceed ${constraint.max}`); - } - } - } - - if (numericErrors.length > 0) { - logWarning('Validation failed - numeric constraints', { - numericErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Numeric validation failed', - { errors: numericErrors } - ); - } - - next(); - }; - } - - /** - * Validates that arrays are not empty - * @param arrayFields Array of field names that should contain non-empty arrays - */ - static validateNonEmptyArrays(arrayFields: string[]) { - return (req: Request, res: Response, next: NextFunction) => { - const arrayErrors: string[] = []; - - for (const field of arrayFields) { - if (req.body && Array.isArray(req.body[field])) { - if (req.body[field].length === 0) { - arrayErrors.push(`Field '${field}' must not be empty`); - } - } else if (req.body && req.body[field] !== undefined) { - arrayErrors.push(`Field '${field}' must be an array`); - } - } - - if (arrayErrors.length > 0) { - logWarning('Validation failed - empty arrays', { - arrayErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Array validation failed', - { errors: arrayErrors } - ); - } - - next(); - }; - } - - /** - * Validates allowed values for enum-like fields - * @param allowedValues Object mapping field names to arrays of allowed values - */ - static validateAllowedValues(allowedValues: Record) { - return (req: Request, res: Response, next: NextFunction) => { - const valueErrors: string[] = []; - - for (const [field, allowed] of Object.entries(allowedValues)) { - if (req.body && req.body[field] !== undefined) { - if (!allowed.includes(req.body[field])) { - valueErrors.push(`Field '${field}' must be one of: ${allowed.join(', ')}`); - } - } - } - - if (valueErrors.length > 0) { - logWarning('Validation failed - disallowed values', { - valueErrors, - endpoint: req.path - }, req, res); - return ErrorResponseService.sendBadRequest( - res, - 'Value validation failed', - { errors: valueErrors } - ); - } - - next(); - }; - } - - /** - * Combines multiple validation middlewares - * @param validations Array of validation middleware functions - */ - static combine(validations: Array<(req: Request, res: Response, next: NextFunction) => void>) { - return async (req: Request, res: Response, next: NextFunction) => { - let currentIndex = 0; - - const runNext = (error?: any) => { - if (error) { - return next(error); - } - - if (currentIndex >= validations.length) { - return next(); - } - - const currentValidation = validations[currentIndex++]; - - try { - currentValidation(req, res, (err?: any) => { - if (res.headersSent) { - return; // Response already sent, don't continue - } - runNext(err); - }); - } catch (error) { - logError('Validation middleware error', error as Error, req, res); - ErrorResponseService.sendInternalServerError(res); - } - }; - - runNext(); - }; - } - - /** - * Helper method to get nested values from request - * @param req Request object - * @param path Dot-notation path like 'body.user.id' - */ - private static getNestedValue(req: Request, path: string): any { - const parts = path.split('.'); - let current: any = req; - - for (const part of parts) { - if (current && typeof current === 'object') { - current = current[part]; - } else { - return undefined; - } - } - - return current; - } -} diff --git a/SerpentRace_Backend/src/Application/Services/WebSocketService.ts b/SerpentRace_Backend/src/Application/Services/WebSocketService.ts deleted file mode 100644 index 5ef871f1..00000000 --- a/SerpentRace_Backend/src/Application/Services/WebSocketService.ts +++ /dev/null @@ -1,1408 +0,0 @@ -import { Server as HttpServer } from 'http'; -import { Server as SocketIOServer, Socket } from 'socket.io'; -import { JWTService, TokenPayload } from './JWTService'; -import { ChatRepository } from '../../Infrastructure/Repository/ChatRepository'; -import { ChatArchiveRepository } from '../../Infrastructure/Repository/ChatArchiveRepository'; -import { UserRepository } from '../../Infrastructure/Repository/UserRepository'; -import { ChatAggregate, ChatType, ChatTypeType, Message } from '../../Domain/Chat/ChatAggregate'; -import { UserState } from '../../Domain/User/UserAggregate'; -import { logAuth, logError, logRequest, logWarning } from './Logger'; -import { RedisService, ActiveChatData } from './RedisService'; -import { v4 as uuidv4 } from 'uuid'; - -interface AuthenticatedSocket extends Socket { - userId?: string; - authLevel?: 0 | 1; - userStatus?: UserState; - orgId?: string | null; -} - -interface JoinChatData { - chatId: string; -} - -interface SendMessageData { - chatId: string; - message: string; -} - -interface CreateGroupData { - name: string; - userIds: string[]; -} - -interface CreateDirectChatData { - targetUserId: string; -} - -interface CreateGameChatData { - gameId: string; - gameName: string; - playerIds: string[]; -} - -interface DeleteChatData { - chatId: string; -} - -interface DeleteChatArchiveData { - archiveId: string; -} - -interface DeleteMessageData { - chatId: string; - messageId: string; -} - -// Game-related WebSocket interfaces (prepared for future implementation) -interface JoinGameRoomData { - gameCode: string; -} - -interface LeaveGameRoomData { - gameCode: string; -} - -interface GameStateUpdateData { - gameId: string; - gameCode: string; - players: string[]; - state: string; - currentTurn?: string; -} - -interface GameActionData { - gameId: string; - gameCode: string; - playerId: string; - action: 'pick_card' | 'play_card' | 'end_turn' | 'leave_game'; - data?: any; -} - -export class WebSocketService { - private io: SocketIOServer; - private jwtService: JWTService; - private chatRepository: ChatRepository; - private chatArchiveRepository: ChatArchiveRepository; - private userRepository: UserRepository; - private redisService: RedisService; - private connectedUsers: Map = new Map(); - private chatTimeout: number; - private maxMessagesPerUser: number; - private messageCleanupWeeks: number; - private userMessageCounts: Map = new Map(); - - constructor(httpServer: HttpServer) { - this.io = new SocketIOServer(httpServer, { - cors: { - origin: ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080'], - methods: ['GET', 'POST'], - credentials: true - } - }); - - this.jwtService = new JWTService(); - this.chatRepository = new ChatRepository(); - this.chatArchiveRepository = new ChatArchiveRepository(); - this.userRepository = new UserRepository(); - this.redisService = RedisService.getInstance(); - this.chatTimeout = parseInt(process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'); - this.maxMessagesPerUser = parseInt(process.env.CHAT_MAX_MESSAGES_PER_USER || '100'); - this.messageCleanupWeeks = parseInt(process.env.CHAT_MESSAGE_CLEANUP_WEEKS || '4'); - - // Initialize Redis connection - this.initializeRedis(); - - this.setupSocketHandlers(); - this.setupArchivingScheduler(); - - logRequest('WebSocket service initialized', undefined, undefined, { - chatTimeoutMinutes: this.chatTimeout - }); - } - - private async initializeRedis(): Promise { - try { - await this.redisService.connect(); - } catch (error) { - logError('Failed to initialize Redis connection', error as Error); - } - } - - private setupSocketHandlers() { - this.io.use(async (socket: AuthenticatedSocket, next) => { - try { - const token = socket.handshake.auth.token || socket.handshake.headers.cookie - ?.split(';') - .find(c => c.trim().startsWith('auth_token=')) - ?.split('=')[1]; - - if (!token) { - logWarning('WebSocket connection rejected - No token provided', { - socketId: socket.id, - ip: socket.handshake.address - }); - return next(new Error('Authentication required')); - } - - // Create a mock request object for JWT verification - const mockRequest = { - headers: { - authorization: `Bearer ${token}`, - cookie: `auth_token=${token}` - }, - cookies: { - auth_token: token - } - } as any; - - const payload = this.jwtService.verify(mockRequest); - if (!payload) { - logWarning('WebSocket connection rejected - Invalid token', { - socketId: socket.id, - ip: socket.handshake.address - }); - return next(new Error('Invalid token')); - } - - socket.userId = payload.userId; - socket.authLevel = payload.authLevel; - socket.userStatus = payload.userStatus; - socket.orgId = payload.orgId; - - logAuth('WebSocket connection authenticated', payload.userId, { - socketId: socket.id, - authLevel: payload.authLevel, - userStatus: payload.userStatus, - orgId: payload.orgId - }); - - next(); - } catch (error) { - logError('WebSocket authentication error', error as Error); - next(new Error('Authentication failed')); - } - }); - - this.io.on('connection', (socket: AuthenticatedSocket) => { - this.handleConnection(socket); - }); - } - - private async handleConnection(socket: AuthenticatedSocket) { - const userId = socket.userId!; - - // Store connected user - this.connectedUsers.set(userId, socket); - - // Load user's active chats and join rooms - try { - const userChats = await this.chatRepository.findActiveChatsForUser(userId); - const chatIds = userChats.map(chat => chat.id); - - // Join all chat rooms - chatIds.forEach(chatId => { - socket.join(chatId); - }); - - // Store user's chat memberships in Redis - await this.redisService.setActiveUser(userId, { - userId, - activeChatIds: chatIds, - lastActivity: new Date(), - isOnline: true - }); - - // Also store each active chat in Redis - for (const chat of userChats) { - await this.redisService.setActiveChat(chat.id, { - chatId: chat.id, - participants: chat.users, - lastActivity: chat.lastActivity || new Date(), - messageCount: chat.messages.length, - chatType: chat.type as 'direct' | 'group' | 'game', - gameId: chat.gameId || undefined, - name: chat.name || undefined - }); - } - - logAuth('User connected to WebSocket', userId, { - socketId: socket.id, - activeChats: chatIds.length - }); - - // Send user their active chats with unread counts - const chatsWithUnread = await Promise.all(userChats.map(async (chat) => ({ - id: chat.id, - type: chat.type, - name: chat.name, - gameId: chat.gameId, - users: chat.users, - lastActivity: chat.lastActivity, - unreadCount: this.calculateUnreadMessages(chat, userId), - isArchived: false - }))); - - socket.emit('chats:list', chatsWithUnread); - - } catch (error) { - logError('Error loading user chats on connection', error as Error, undefined, undefined); - socket.emit('error', { message: 'Failed to load chats' }); - } - - // Setup event handlers - socket.on('chat:join', (data: JoinChatData) => this.handleJoinChat(socket, data)); - socket.on('chat:leave', (data: JoinChatData) => this.handleLeaveChat(socket, data)); - socket.on('message:send', (data: SendMessageData) => this.handleSendMessage(socket, data)); - socket.on('group:create', (data: CreateGroupData) => this.handleCreateGroup(socket, data)); - socket.on('chat:direct', (data: CreateDirectChatData) => this.handleCreateDirectChat(socket, data)); - socket.on('game:chat:create', (data: CreateGameChatData) => this.handleCreateGameChat(socket, data)); - socket.on('chat:history', (data: JoinChatData) => this.handleGetChatHistory(socket, data)); - socket.on('chat:delete', (data: DeleteChatData) => this.handleDeleteChat(socket, data)); - socket.on('chat:archive:delete', (data: DeleteChatArchiveData) => this.handleDeleteChatArchive(socket, data)); - socket.on('message:delete', (data: DeleteMessageData) => this.handleDeleteMessage(socket, data)); - - socket.on('disconnect', () => this.handleDisconnection(socket)); - } - - private async handleJoinChat(socket: AuthenticatedSocket, data: JoinChatData) { - try { - const userId = socket.userId!; - const chat = await this.chatRepository.findById(data.chatId); - - if (!chat) { - socket.emit('error', { message: 'Chat not found' }); - return; - } - - // Check if user is member of this chat - if (!chat.users.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to join this chat' }); - return; - } - - // Join the chat room - socket.join(data.chatId); - - // Add to user's active chats in Redis - await this.redisService.addUserToChat(userId, data.chatId); - - // Update chat activity in Redis - await this.redisService.updateChatActivity(data.chatId); - - // Update last activity in database - await this.chatRepository.update(data.chatId, { lastActivity: new Date() }); - - logAuth('User joined chat', userId, { - chatId: data.chatId, - chatType: chat.type - }); - - socket.emit('chat:joined', { - chatId: data.chatId, - messages: chat.messages.slice(-10) // Last 10 messages - }); - - } catch (error) { - logError('Error joining chat', error as Error); - socket.emit('error', { message: 'Failed to join chat' }); - } - } - - private async handleLeaveChat(socket: AuthenticatedSocket, data: JoinChatData) { - try { - const userId = socket.userId!; - - // Leave the chat room - socket.leave(data.chatId); - - // Remove from user's active chats in Redis - await this.redisService.removeUserFromChat(userId, data.chatId); - - logAuth('User left chat', userId, { - chatId: data.chatId - }); - - socket.emit('chat:left', { chatId: data.chatId }); - - } catch (error) { - logError('Error leaving chat', error as Error); - socket.emit('error', { message: 'Failed to leave chat' }); - } - } - - private async handleSendMessage(socket: AuthenticatedSocket, data: SendMessageData) { - try { - const userId = socket.userId!; - - // Rate limiting check - if (!this.checkMessageRateLimit(userId)) { - socket.emit('error', { message: `Rate limit exceeded. Maximum ${this.maxMessagesPerUser} messages per minute allowed.` }); - return; - } - - // Validate message is string and not empty - if (typeof data.message !== 'string' || !data.message.trim()) { - socket.emit('error', { message: 'Message must be a non-empty string' }); - return; - } - - const chat = await this.chatRepository.findById(data.chatId); - if (!chat) { - socket.emit('error', { message: 'Chat not found' }); - return; - } - - // Check if user is member of this chat - if (!chat.users.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to send message to this chat' }); - return; - } - - // Create message - const message: Message = { - id: uuidv4(), - date: new Date(), - userid: userId, - text: data.message.trim() - }; - - // Manage message history based on chat type - let updatedMessages = [...chat.messages, message]; - updatedMessages = this.pruneMessages(updatedMessages, chat.type); - - // Update chat - await this.chatRepository.update(data.chatId, { - messages: updatedMessages, - lastActivity: new Date() - }); - - // Update chat activity in Redis with new message count - await this.redisService.updateChatActivity(data.chatId, updatedMessages.length); - - // Broadcast to all users in the chat room - this.io.to(data.chatId).emit('message:received', { - chatId: data.chatId, - message: message - }); - - // Send notifications to offline users - await this.notifyOfflineUsers(chat, message); - - logAuth('Message sent', userId, { - chatId: data.chatId, - messageLength: data.message.length, - chatType: chat.type - }); - - } catch (error) { - logError('Error sending message', error as Error); - socket.emit('error', { message: 'Failed to send message' }); - } - } - - private async handleCreateGroup(socket: AuthenticatedSocket, data: CreateGroupData) { - try { - const userId = socket.userId!; - - // Check if user is premium (required to create groups) - const user = await this.userRepository.findById(userId); - if (!user || user.state !== UserState.VERIFIED_PREMIUM) { - socket.emit('error', { message: 'Premium subscription required to create groups' }); - return; - } - - // Validate group data - if (!data.name?.trim()) { - socket.emit('error', { message: 'Group name is required' }); - return; - } - - if (!data.userIds || data.userIds.length === 0) { - socket.emit('error', { message: 'At least one member is required' }); - return; - } - - // Verify all users exist - const members = await Promise.all( - data.userIds.map(id => this.userRepository.findById(id)) - ); - - if (members.some(member => !member)) { - socket.emit('error', { message: 'One or more users not found' }); - return; - } - - // Create group chat - const groupChat = await this.chatRepository.create({ - type: ChatType.GROUP, - name: data.name.trim(), - createdBy: userId, - users: [userId, ...data.userIds], // Include creator - messages: [], - lastActivity: new Date() - }); - - // Add all members to the group room and store in Redis - const allMemberIds = data.userIds.concat(userId); - for (const memberId of allMemberIds) { - const memberSocket = this.connectedUsers.get(memberId); - if (memberSocket) { - memberSocket.join(groupChat.id); - } - - // Update user's chat list in Redis - await this.redisService.addUserToChat(memberId, groupChat.id); - } - - // Store the group chat in Redis - await this.redisService.setActiveChat(groupChat.id, { - chatId: groupChat.id, - participants: allMemberIds, - lastActivity: new Date(), - messageCount: 0, - chatType: 'group', - name: groupChat.name || undefined - }); - - // Notify all members - this.io.to(groupChat.id).emit('group:created', { - chat: { - id: groupChat.id, - type: groupChat.type, - name: groupChat.name, - createdBy: groupChat.createdBy, - users: groupChat.users, - messages: [] - } - }); - - logAuth('Group created', userId, { - groupId: groupChat.id, - groupName: data.name, - memberCount: groupChat.users.length - }); - - } catch (error) { - logError('Error creating group', error as Error); - socket.emit('error', { message: 'Failed to create group' }); - } - } - - private async handleCreateDirectChat(socket: AuthenticatedSocket, data: CreateDirectChatData) { - try { - const userId = socket.userId!; - - // Validate target user exists - const targetUser = await this.userRepository.findById(data.targetUserId); - if (!targetUser) { - socket.emit('error', { message: 'Target user not found' }); - return; - } - - // Check if direct chat already exists - const existingChats = await this.chatRepository.findByUserId(userId); - const existingDirectChat = existingChats.find(chat => - chat.type === ChatType.DIRECT && - chat.users.length === 2 && - chat.users.includes(data.targetUserId) - ); - - if (existingDirectChat) { - socket.emit('chat:direct:exists', { - chatId: existingDirectChat.id - }); - return; - } - - // Create direct chat - const directChat = await this.chatRepository.create({ - type: ChatType.DIRECT, - users: [userId, data.targetUserId], - messages: [], - lastActivity: new Date() - }); - - // Add both users to the chat room if they're online and store in Redis - const memberIds = [userId, data.targetUserId]; - for (const memberId of memberIds) { - const memberSocket = this.connectedUsers.get(memberId); - if (memberSocket) { - memberSocket.join(directChat.id); - } - - // Update user's chat list in Redis - await this.redisService.addUserToChat(memberId, directChat.id); - } - - // Store the direct chat in Redis - await this.redisService.setActiveChat(directChat.id, { - chatId: directChat.id, - participants: memberIds, - lastActivity: new Date(), - messageCount: 0, - chatType: 'direct' - }); - - // Notify both users - this.io.to(directChat.id).emit('chat:direct:created', { - chat: { - id: directChat.id, - type: directChat.type, - users: directChat.users, - messages: [] - } - }); - - logAuth('Direct chat created', userId, { - chatId: directChat.id, - targetUserId: data.targetUserId - }); - - } catch (error) { - logError('Error creating direct chat', error as Error); - socket.emit('error', { message: 'Failed to create direct chat' }); - } - } - - private async handleCreateGameChat(socket: AuthenticatedSocket, data: CreateGameChatData) { - try { - const userId = socket.userId!; - - // Check if game chat already exists - const existingGameChat = await this.chatRepository.findByGameId(data.gameId); - if (existingGameChat) { - socket.emit('game:chat:exists', { - chatId: existingGameChat.id - }); - return; - } - - // Create game chat - const gameChat = await this.chatRepository.create({ - type: ChatType.GAME, - name: data.gameName, - gameId: data.gameId, - users: data.playerIds, - messages: [], - lastActivity: new Date() - }); - - // Add all players to the game chat room if they're online and store in Redis - for (const playerId of data.playerIds) { - const playerSocket = this.connectedUsers.get(playerId); - if (playerSocket) { - playerSocket.join(gameChat.id); - } - - // Update user's chat list in Redis - await this.redisService.addUserToChat(playerId, gameChat.id); - } - - // Store the game chat in Redis - await this.redisService.setActiveChat(gameChat.id, { - chatId: gameChat.id, - participants: data.playerIds, - lastActivity: new Date(), - messageCount: 0, - chatType: 'game', - gameId: gameChat.gameId || undefined, - name: gameChat.name || undefined - }); - - // Notify all players - this.io.to(gameChat.id).emit('game:chat:created', { - chat: { - id: gameChat.id, - type: gameChat.type, - name: gameChat.name, - gameId: gameChat.gameId, - users: gameChat.users, - messages: [] - } - }); - - logAuth('Game chat created', userId, { - chatId: gameChat.id, - gameId: data.gameId, - gameName: data.gameName, - playerCount: data.playerIds.length - }); - - } catch (error) { - logError('Error creating game chat', error as Error); - socket.emit('error', { message: 'Failed to create game chat' }); - } - } - - private async handleGetChatHistory(socket: AuthenticatedSocket, data: JoinChatData) { - try { - const userId = socket.userId!; - const chat = await this.chatRepository.findById(data.chatId); - - if (!chat) { - // Check if it's archived - const archived = await this.chatRepository.getArchivedChat(data.chatId); - if (archived) { - socket.emit('chat:history:archived', { - chatId: data.chatId, - messages: archived.archivedMessages, - chatType: archived.chatType, - isGameChat: archived.chatType === ChatType.GAME - }); - } else { - socket.emit('error', { message: 'Chat not found' }); - } - return; - } - - // Check if user has access - if (!chat.users.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to view this chat' }); - return; - } - - socket.emit('chat:history', { - chatId: data.chatId, - messages: chat.messages, - chatInfo: { - type: chat.type, - name: chat.name, - gameId: chat.gameId, - users: chat.users - } - }); - - } catch (error) { - logError('Error getting chat history', error as Error); - socket.emit('error', { message: 'Failed to get chat history' }); - } - } - - private async handleDeleteChat(socket: AuthenticatedSocket, data: DeleteChatData) { - try { - const userId = socket.userId!; - const chat = await this.chatRepository.findById(data.chatId); - - if (!chat) { - socket.emit('error', { message: 'Chat not found' }); - return; - } - - // Check if user is member of this chat - if (!chat.users.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to delete this chat' }); - return; - } - - // Perform soft delete - const deletedChat = await this.chatRepository.softDelete(data.chatId); - if (!deletedChat) { - socket.emit('error', { message: 'Failed to delete chat' }); - return; - } - - // Remove from Redis active chats - await this.redisService.removeActiveChat(data.chatId); - - // Notify all participants that the chat has been deleted - this.io.to(data.chatId).emit('chat:deleted', { - chatId: data.chatId, - deletedBy: userId - }); - - // Remove all users from the chat room - for (const participantId of chat.users) { - const participantSocket = this.connectedUsers.get(participantId); - if (participantSocket) { - participantSocket.leave(data.chatId); - } - // Remove from user's active chats in Redis - await this.redisService.removeUserFromChat(participantId, data.chatId); - } - - logAuth('Chat deleted', userId, { - chatId: data.chatId, - chatType: chat.type, - participantCount: chat.users.length - }); - - socket.emit('chat:delete:success', { - chatId: data.chatId, - message: 'Chat deleted successfully' - }); - - } catch (error) { - logError('Error deleting chat', error as Error); - socket.emit('error', { message: 'Failed to delete chat' }); - } - } - - private async handleDeleteChatArchive(socket: AuthenticatedSocket, data: DeleteChatArchiveData) { - try { - const userId = socket.userId!; - const archive = await this.chatArchiveRepository.findById(data.archiveId); - - if (!archive) { - socket.emit('error', { message: 'Chat archive not found' }); - return; - } - - // Check if user was a participant in the archived chat - if (!archive.participants.includes(userId)) { - socket.emit('error', { message: 'Unauthorized to delete this chat archive' }); - return; - } - - // Hard delete the archive (since it's already archived) - await this.chatArchiveRepository.delete(data.archiveId); - - logAuth('Chat archive deleted', userId, { - archiveId: data.archiveId, - originalChatId: archive.chatId, - chatType: archive.chatType, - participantCount: archive.participants.length - }); - - socket.emit('chat:archive:delete:success', { - archiveId: data.archiveId, - message: 'Chat archive deleted successfully' - }); - - } catch (error) { - logError('Error deleting chat archive', error as Error); - socket.emit('error', { message: 'Failed to delete chat archive' }); - } - } - - private async handleDeleteMessage(socket: AuthenticatedSocket, data: DeleteMessageData) { - try { - const userId = socket.userId!; - - // Check if user has admin/moderator privileges - const user = await this.userRepository.findById(userId); - if (!user || user.state !== UserState.ADMIN) { // Check if user is admin - socket.emit('error', { message: 'Insufficient permissions to delete messages' }); - return; - } - - const success = await this.deleteMessage(data.chatId, data.messageId, userId); - if (success) { - socket.emit('message:delete:success', { - chatId: data.chatId, - messageId: data.messageId, - message: 'Message deleted successfully' - }); - } else { - socket.emit('error', { message: 'Failed to delete message or message not found' }); - } - - } catch (error) { - logError('Error handling delete message request', error as Error); - socket.emit('error', { message: 'Failed to delete message' }); - } - } - - private async handleDisconnection(socket: AuthenticatedSocket) { - const userId = socket.userId; - if (userId) { - this.connectedUsers.delete(userId); - - // Update user status in Redis - const userData = await this.redisService.getActiveUser(userId); - if (userData) { - userData.isOnline = false; - userData.lastActivity = new Date(); - await this.redisService.setActiveUser(userId, userData); - } - - logAuth('User disconnected from WebSocket', userId, { - socketId: socket.id - }); - } - } - - // Utility methods - private calculateUnreadMessages(chat: ChatAggregate, userId: string): number { - // Simple implementation - count messages after user's last seen - // In production, you'd store lastSeen timestamp per user per chat - return chat.messages.filter(msg => msg.userid !== userId).length; - } - - private pruneMessages(messages: Message[], chatType: ChatTypeType): Message[] { - const twoWeeksAgo = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000); - - // Remove messages older than 2 weeks - let prunedMessages = messages.filter(msg => new Date(msg.date) > twoWeeksAgo); - - // For group chats, only apply the 2-week time limit (unlimited messages per user) - if (chatType === ChatType.GROUP) { - return prunedMessages.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - } - - // For direct and game chats, apply both time limit and per-user message limit - // Group by user and keep last 10 messages per user - const messagesByUser = new Map(); - prunedMessages.forEach(msg => { - if (!messagesByUser.has(msg.userid)) { - messagesByUser.set(msg.userid, []); - } - messagesByUser.get(msg.userid)!.push(msg); - }); - - // Keep only last 10 messages per user - const finalMessages: Message[] = []; - messagesByUser.forEach((userMessages, userId) => { - const last10 = userMessages.slice(-10); - finalMessages.push(...last10); - }); - - // Sort by date - return finalMessages.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - } - - private async notifyOfflineUsers(chat: ChatAggregate, message: Message) { - // Find users who are not currently connected - const offlineUsers = chat.users.filter(userId => - userId !== message.userid && !this.connectedUsers.has(userId) - ); - - // In a real implementation, you would send push notifications or emails here - if (offlineUsers.length > 0) { - logRequest('Offline users to notify', undefined, undefined, { - chatId: chat.id, - offlineUserCount: offlineUsers.length, - messageFrom: message.userid - }); - } - } - - private setupArchivingScheduler() { - // Run every hour to check for inactive chats - setInterval(async () => { - try { - // First, cleanup inactive chats from Redis and get their IDs - const inactiveChatIds = await this.redisService.cleanupInactiveChats(this.chatTimeout); - - // Archive the inactive chats in the database - for (const chatId of inactiveChatIds) { - const chat = await this.chatRepository.findById(chatId); - if (chat) { - await this.chatRepository.archiveChat(chat); - logRequest('Chat archived due to inactivity', undefined, undefined, { - chatId: chat.id, - chatType: chat.type, - lastActivity: chat.lastActivity, - messageCount: chat.messages.length - }); - } - } - - // Also find inactive chats from database that might not be in Redis - const dbInactiveChats = await this.chatRepository.findInactiveChats(this.chatTimeout); - const additionalInactiveChats = dbInactiveChats.filter(chat => - !inactiveChatIds.includes(chat.id) - ); - - for (const chat of additionalInactiveChats) { - await this.chatRepository.archiveChat(chat); - logRequest('Chat archived due to inactivity (from DB)', undefined, undefined, { - chatId: chat.id, - chatType: chat.type, - lastActivity: chat.lastActivity, - messageCount: chat.messages.length - }); - } - - const totalArchived = inactiveChatIds.length + additionalInactiveChats.length; - if (totalArchived > 0) { - logRequest('Chat archiving completed', undefined, undefined, { - archivedCount: totalArchived, - redisCleanedUp: inactiveChatIds.length, - databaseCleanedUp: additionalInactiveChats.length, - timeoutMinutes: this.chatTimeout - }); - } - - // Cleanup old messages from archived chats based on messageCleanupWeeks - await this.cleanupOldMessages(); - - } catch (error) { - logError('Error in chat archiving scheduler', error as Error); - } - }, 60 * 60 * 1000); // 1 hour - - // Also run message count cleanup every 5 minutes - setInterval(() => { - this.cleanupMessageCounts(); - }, 5 * 60 * 1000); // 5 minutes - } - - // Public methods for game integration - public async createGameChat(gameId: string, gameName: string, playerIds: string[]): Promise { - try { - const existingGameChat = await this.chatRepository.findByGameId(gameId); - if (existingGameChat) { - return existingGameChat; - } - - const gameChat = await this.chatRepository.create({ - type: ChatType.GAME, - name: gameName, - gameId: gameId, - users: playerIds, - messages: [], - lastActivity: new Date() - }); - - // Notify connected players - playerIds.forEach(playerId => { - const playerSocket = this.connectedUsers.get(playerId); - if (playerSocket) { - playerSocket.join(gameChat.id); - playerSocket.emit('game:chat:created', { - chat: { - id: gameChat.id, - type: gameChat.type, - name: gameChat.name, - gameId: gameChat.gameId, - users: gameChat.users, - messages: [] - } - }); - } - }); - - return gameChat; - } catch (error) { - logError('Error creating game chat programmatically', error as Error); - return null; - } - } - - public getConnectedUserCount(): number { - return this.connectedUsers.size; - } - - public isUserConnected(userId: string): boolean { - return this.connectedUsers.has(userId); - } - - public async cleanup(): Promise { - try { - await this.redisService.disconnect(); - } catch (error) { - logError('Error during WebSocket service cleanup', error as Error); - } - } - - /** - * Manually trigger cleanup of old messages and chats - * This can be called by admin endpoints for maintenance - */ - public async triggerManualCleanup(): Promise<{ deletedArchives: number; deletedChats: number }> { - try { - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - (this.messageCleanupWeeks * 7)); - - // Clean up old archived messages - const deletedArchivesCount = await this.chatArchiveRepository.cleanup(this.messageCleanupWeeks * 7); - - // Clean up soft-deleted chats - const softDeletedChats = await this.chatRepository.findByPageIncludingDeleted(0, 1000); - let deletedChatsCount = 0; - - for (const chat of softDeletedChats.chats) { - if (chat.state === 2 && chat.updateDate < cutoffDate) { // SOFT_DELETE state = 2 - await this.chatRepository.delete(chat.id); // Hard delete - deletedChatsCount++; - } - } - - logRequest('Manual cleanup triggered', undefined, undefined, { - cutoffDate: cutoffDate.toISOString(), - cleanupWeeks: this.messageCleanupWeeks, - deletedArchives: deletedArchivesCount, - deletedChats: deletedChatsCount, - triggeredBy: 'manual' - }); - - return { deletedArchives: deletedArchivesCount, deletedChats: deletedChatsCount }; - - } catch (error) { - logError('Error during manual cleanup', error as Error); - throw error; - } - } - - /** - * Clean up old messages from archived chats based on messageCleanupWeeks setting - */ - private async cleanupOldMessages(): Promise { - try { - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - (this.messageCleanupWeeks * 7)); - - // Clean up old archived messages using ChatArchiveRepository - const deletedArchivesCount = await this.chatArchiveRepository.cleanup(this.messageCleanupWeeks * 7); - - // Also clean up soft-deleted chats from the main repository - // Get all soft-deleted chats that are older than the cleanup period - const softDeletedChats = await this.chatRepository.findByPageIncludingDeleted(0, 1000); - let deletedChatsCount = 0; - - for (const chat of softDeletedChats.chats) { - if (chat.state === 2 && chat.updateDate < cutoffDate) { // SOFT_DELETE state = 2 - await this.chatRepository.delete(chat.id); // Hard delete - deletedChatsCount++; - } - } - - logRequest('Old message cleanup completed', undefined, undefined, { - cutoffDate: cutoffDate.toISOString(), - cleanupWeeks: this.messageCleanupWeeks, - deletedArchives: deletedArchivesCount, - deletedChats: deletedChatsCount, - note: 'Cleanup completed using both ChatRepository and ChatArchiveRepository' - }); - - } catch (error) { - logError('Error cleaning up old messages', error as Error); - } - } - - /** - * Check if user has exceeded message rate limit - * @param userId User ID to check - * @returns true if within limit, false if exceeded - */ - private checkMessageRateLimit(userId: string): boolean { - const now = Date.now(); - const minute = 60 * 1000; // 1 minute in milliseconds - - const userStats = this.userMessageCounts.get(userId) || { count: 0, lastReset: now }; - - // Reset counter if more than a minute has passed - if (now - userStats.lastReset >= minute) { - userStats.count = 0; - userStats.lastReset = now; - } - - // Check if user is within limits - if (userStats.count >= this.maxMessagesPerUser) { - return false; - } - - // Increment counter - userStats.count++; - this.userMessageCounts.set(userId, userStats); - - return true; - } - - /** - * Delete a specific message from chat history - * This can be used for moderation purposes - */ - public async deleteMessage(chatId: string, messageId: string, moderatorUserId: string): Promise { - try { - // Get the chat - const chat = await this.chatRepository.findById(chatId); - if (!chat) { - // Check archived chats - const archivedChat = await this.chatRepository.getArchivedChat(chatId); - if (!archivedChat) { - logWarning('Chat not found for message deletion', { - chatId, - messageId, - moderatorUserId - }); - return false; - } - - // Remove message from archived chat - const updatedMessages = archivedChat.archivedMessages.filter(msg => msg.id !== messageId); - if (updatedMessages.length === archivedChat.archivedMessages.length) { - logWarning('Message not found in archived chat', { - chatId, - messageId, - moderatorUserId - }); - return false; - } - - // Update archived chat - await this.chatArchiveRepository.create({ - ...archivedChat, - archivedMessages: updatedMessages - }); - - logAuth('Message deleted from archived chat', moderatorUserId, { - chatId, - messageId, - originalMessageCount: archivedChat.archivedMessages.length, - newMessageCount: updatedMessages.length - }); - - return true; - } - - // Remove message from active chat - const updatedMessages = chat.messages.filter(msg => msg.id !== messageId); - if (updatedMessages.length === chat.messages.length) { - logWarning('Message not found in active chat', { - chatId, - messageId, - moderatorUserId - }); - return false; - } - - // Update active chat - await this.chatRepository.update(chatId, { - messages: updatedMessages - }); - - // Notify all users in the chat about message deletion - this.io.to(chatId).emit('message:deleted', { - chatId, - messageId, - deletedBy: moderatorUserId - }); - - logAuth('Message deleted from active chat', moderatorUserId, { - chatId, - messageId, - originalMessageCount: chat.messages.length, - newMessageCount: updatedMessages.length - }); - - return true; - - } catch (error) { - logError('Error deleting message', error as Error); - return false; - } - } - - /** - * Clean up old user message count entries (called periodically) - */ - private cleanupMessageCounts(): void { - const now = Date.now(); - const minute = 60 * 1000; - - for (const [userId, stats] of this.userMessageCounts.entries()) { - if (now - stats.lastReset >= minute * 5) { // Keep for 5 minutes - this.userMessageCounts.delete(userId); - } - } - } - - // Game-related WebSocket handlers (prepared for future implementation) - - /** - * Handle player joining a game room for real-time updates - * @param socket The authenticated socket - * @param data Game room data containing game code - */ - private async handleJoinGameRoom(socket: AuthenticatedSocket, data: JoinGameRoomData) { - try { - const userId = socket.userId!; - const gameRoom = `game_${data.gameCode}`; - - logAuth('Player joining game room', userId, { - gameCode: data.gameCode, - gameRoom, - socketId: socket.id - }); - - // Join the WebSocket room for this game - await socket.join(gameRoom); - - // Emit confirmation to the player - socket.emit('game:joined', { - gameCode: data.gameCode, - room: gameRoom, - message: 'Successfully joined game room' - }); - - // Notify other players in the game room - socket.to(gameRoom).emit('game:player_joined', { - playerId: userId, - gameCode: data.gameCode, - timestamp: new Date().toISOString() - }); - - logAuth('Player joined game room successfully', userId, { - gameCode: data.gameCode, - gameRoom - }); - - } catch (error) { - logError('Error joining game room', error as Error); - socket.emit('game:error', { - message: 'Failed to join game room', - gameCode: data.gameCode - }); - } - } - - /** - * Handle player leaving a game room - * @param socket The authenticated socket - * @param data Game room data containing game code - */ - private async handleLeaveGameRoom(socket: AuthenticatedSocket, data: LeaveGameRoomData) { - try { - const userId = socket.userId!; - const gameRoom = `game_${data.gameCode}`; - - logAuth('Player leaving game room', userId, { - gameCode: data.gameCode, - gameRoom, - socketId: socket.id - }); - - // Leave the WebSocket room - await socket.leave(gameRoom); - - // Notify other players in the game room - socket.to(gameRoom).emit('game:player_left', { - playerId: userId, - gameCode: data.gameCode, - timestamp: new Date().toISOString() - }); - - // Confirm to the leaving player - socket.emit('game:left', { - gameCode: data.gameCode, - message: 'Successfully left game room' - }); - - logAuth('Player left game room successfully', userId, { - gameCode: data.gameCode, - gameRoom - }); - - } catch (error) { - logError('Error leaving game room', error as Error); - socket.emit('game:error', { - message: 'Failed to leave game room', - gameCode: data.gameCode - }); - } - } - - /** - * Handle game actions (cards, turns, etc.) - prepared for future implementation - * @param socket The authenticated socket - * @param data Game action data - */ - private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData) { - try { - const userId = socket.userId!; - const gameRoom = `game_${data.gameCode}`; - - logAuth('Game action received', userId, { - gameId: data.gameId, - gameCode: data.gameCode, - action: data.action, - socketId: socket.id - }); - - // Validate that the player is authorized to perform this action - if (data.playerId !== userId) { - socket.emit('game:error', { - message: 'Unauthorized action', - gameCode: data.gameCode - }); - return; - } - - // TODO: Implement specific game logic here - // This will be implemented when the game flow is discussed - - // For now, just broadcast the action to other players - socket.to(gameRoom).emit('game:action_performed', { - playerId: userId, - gameCode: data.gameCode, - action: data.action, - data: data.data, - timestamp: new Date().toISOString() - }); - - // Confirm action to the acting player - socket.emit('game:action_confirmed', { - gameCode: data.gameCode, - action: data.action, - message: 'Action processed successfully' - }); - - logAuth('Game action processed', userId, { - gameId: data.gameId, - gameCode: data.gameCode, - action: data.action - }); - - } catch (error) { - logError('Error processing game action', error as Error); - socket.emit('game:error', { - message: 'Failed to process game action', - gameCode: data.gameCode, - action: data.action - }); - } - } - - /** - * Broadcast game state updates to all players in a game - * @param gameCode The game code - * @param gameState The updated game state - */ - public broadcastGameStateUpdate(gameCode: string, gameState: GameStateUpdateData): void { - try { - const gameRoom = `game_${gameCode}`; - - this.io.to(gameRoom).emit('game:state_updated', { - ...gameState, - timestamp: new Date().toISOString() - }); - - logRequest('Game state broadcasted', undefined, undefined, { - gameCode, - gameRoom, - playerCount: gameState.players.length - }); - - } catch (error) { - logError('Error broadcasting game state', error as Error); - } - } - - /** - * Notify players when a game starts - * @param gameCode The game code - * @param players Array of player IDs - */ - public notifyGameStart(gameCode: string, players: string[]): void { - try { - const gameRoom = `game_${gameCode}`; - - this.io.to(gameRoom).emit('game:started', { - gameCode, - players, - message: 'Game has started!', - timestamp: new Date().toISOString() - }); - - logRequest('Game start notification sent', undefined, undefined, { - gameCode, - playerCount: players.length - }); - - } catch (error) { - logError('Error notifying game start', error as Error); - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommand.ts deleted file mode 100644 index 0cad88e6..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommand.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ActivateUserCommand { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommandHandler.ts deleted file mode 100644 index b4a22ab0..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/ActivateUserCommandHandler.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { ActivateUserCommand } from './ActivateUserCommand'; - - -export class ActivateUserCommandHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(cmd: ActivateUserCommand): Promise { - await this.userRepo.activate(cmd.id); - return true; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/User/commands/CreateUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/CreateUserCommand.ts deleted file mode 100644 index 25b11ae5..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/CreateUserCommand.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface CreateUserCommand { - username: string; - password: string; - email: string; - fname: string; - lname: string; - code?: string; - orgid?: string; - phone?: string; - language: 'hu' | 'de' | 'en'; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/CreateUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/CreateUserCommandHandler.ts deleted file mode 100644 index 6c9a2cf5..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/CreateUserCommandHandler.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { CreateUserCommand } from './CreateUserCommand'; -import { ShortUserDto } from '../../DTOs/UserDto'; -import { UserAggregate, UserState } from '../../../Domain/User/UserAggregate'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { PasswordService } from '../../Services/PasswordService'; -import { EmailService } from '../../Services/EmailService'; -import { TokenService } from '../../Services/TokenService'; -import { logDatabase, logError, logAuth, logWarning } from '../../Services/Logger'; - -export class CreateUserCommandHandler { - constructor( - private readonly userRepo: IUserRepository, - private readonly emailService: EmailService - ) {} - - async execute(cmd: CreateUserCommand): Promise { - try { - // Validate password strength - const passwordValidation = PasswordService.validatePasswordStrength(cmd.password); - if (!passwordValidation.isValid) { - throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`); - } - - const user = new UserAggregate(); - user.username = cmd.username; - - // Hash the password before storing - user.password = await PasswordService.hashPassword(cmd.password); - - // Generate verification token - const verificationTokenData = TokenService.generateVerificationToken(); - user.token = await TokenService.hashToken(verificationTokenData.token); - user.TokenExpires = verificationTokenData.expiresAt; - - user.email = cmd.email; - user.fname = cmd.fname; - user.lname = cmd.lname; - user.orgid = cmd.orgid || null; - user.phone = cmd.phone || null; - user.state = UserState.REGISTERED_NOT_VERIFIED; - - const created = await this.userRepo.create(user); - - // Send verification email (non-blocking) - this.sendVerificationEmailAsync(cmd.language, created, verificationTokenData.token); - - return UserMapper.toShortDto(created); - } catch (error) { - // Only log the error once here, don't log again in router - const errorMessage = (error as Error).message; - - // Re-throw validation errors as-is (don't log as these are user input errors) - if (errorMessage.includes('Password validation failed')) { - throw error; - } - - // Handle database constraint errors - if (errorMessage.includes('duplicate') || errorMessage.includes('unique') || - errorMessage.includes('UNIQUE constraint') || errorMessage.includes('already exists')) { - throw new Error('User with this username or email already exists'); - } - - // Log database/system errors but throw user-friendly message - logError('CreateUserCommandHandler error', error as Error); - throw new Error('Failed to create user'); - } - } - - private async sendVerificationEmailAsync(language: 'hu' | 'de' | 'en', user: UserAggregate, token: string): Promise { - try { - const baseUrl = process.env.FRONTEND_URL || 'http://localhost:5173'; - const verificationUrl = TokenService.generateVerificationUrl(baseUrl, token); - - const emailSent = await this.emailService.sendVerificationEmail( - user.email, - `${user.fname} ${user.lname}`, - token, - verificationUrl, - language - ); - - if (!emailSent) { - logWarning('Failed to send verification email', { email: user.email, userId: user.id }); - } else { - logAuth('Verification email sent successfully', user.id, { email: user.email }); - } - } catch (emailError) { - logError('Error sending verification email', emailError as Error); - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommand.ts deleted file mode 100644 index 3d24d35c..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommand.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface DeactivateUserCommand { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommandHandler.ts deleted file mode 100644 index 08880f0c..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/DeactivateUserCommandHandler.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { DeactivateUserCommand } from './DeactivateUserCommand'; - - -export class DeactivateUserCommandHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(cmd: DeactivateUserCommand): Promise { - await this.userRepo.deactivate(cmd.id); - return true; - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommand.ts deleted file mode 100644 index 22de9f40..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface DeleteUserCommand { - id: string; - soft?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommandHandler.ts deleted file mode 100644 index eb1af0ae..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/DeleteUserCommandHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { DeleteUserCommand } from './DeleteUserCommand'; - - -export class DeleteUserCommandHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(cmd: DeleteUserCommand): Promise { - if (cmd.soft) { - await this.userRepo.softDelete(cmd.id); - } else { - await this.userRepo.delete(cmd.id); - } - return true; - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/LoginCommand.ts b/SerpentRace_Backend/src/Application/User/commands/LoginCommand.ts deleted file mode 100644 index b4861329..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/LoginCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface LoginCommand { - username: string; - password: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/LoginCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/LoginCommandHandler.ts deleted file mode 100644 index a7df6f72..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/LoginCommandHandler.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository'; -import { LoginCommand } from './LoginCommand'; -import { ShortUserDto } from '../../DTOs/UserDto'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { PasswordService } from '../../Services/PasswordService'; -import { JWTService } from '../../Services/JWTService'; -import { UserState } from '../../../Domain/User/UserAggregate'; -import { logAuth, logDatabase, logError, logWarning } from '../../Services/Logger'; -import { Response } from 'express'; - -export interface LoginResponse { - user: ShortUserDto; - token?: string; - refreshToken?: string; - requiresOrgReauth?: boolean; - orgLoginUrl?: string; - organizationName?: string; -} - -export class LoginCommandHandler { - constructor( - private readonly userRepo: IUserRepository, - private readonly jwtService: JWTService, - private readonly orgRepo: IOrganizationRepository - ) {} - - async execute(cmd: LoginCommand, res?: Response): Promise { - const startTime = Date.now(); - - try { - logAuth('Login attempt', undefined, { username: cmd.username }); - - const user = await this.userRepo.findByUsername(cmd.username) || - await this.userRepo.findByEmail(cmd.username); - - logDatabase('User lookup completed', undefined, Date.now() - startTime, { - found: !!user, - searchBy: cmd.username.includes('@') ? 'email' : 'username' - }); - - if (!user) { - logAuth('Login failed - User not found', undefined, { username: cmd.username }); - throw new Error('Invalid username'); - } - - // Check if user account state allows login - const restrictedStates = [ - UserState.REGISTERED_NOT_VERIFIED, - UserState.SOFT_DELETE, - UserState.DEACTIVATED - ]; - - if (restrictedStates.includes(user.state)) { - let stateDescription = ''; - let errorMessage = ''; - switch (user.state) { - case UserState.REGISTERED_NOT_VERIFIED: - stateDescription = 'Email not verified'; - errorMessage = 'User account not verified'; - break; - case UserState.SOFT_DELETE: - stateDescription = 'Account deleted'; - errorMessage = 'User account deactivated'; - break; - case UserState.DEACTIVATED: - stateDescription = 'Account deactivated'; - errorMessage = 'User account deactivated'; - break; - } - - logAuth('Login failed - Account state restriction', user.id, { - username: cmd.username, - userState: user.state, - stateDescription - }); - throw new Error(errorMessage); - } - - try { - const passwordStartTime = Date.now(); - const isPasswordValid = await PasswordService.verifyPassword(cmd.password, user.password); - - logAuth('Password verification completed', user.id, { - valid: isPasswordValid, - verificationTime: Date.now() - passwordStartTime - }); - - if (!isPasswordValid) { - logWarning('Login failed - Invalid password', { - userId: user.id, - username: cmd.username - }); - throw new Error('Invalid password'); - } - } catch (error) { - logError('Password verification error', error as Error); - throw new Error('Invalid password'); - } - - const mockRes = { - cookie: () => {} - } as any; - - const tokenPayload = { - userId: user.id, - authLevel: (user.state === UserState.ADMIN ? 1 : 0) as 0 | 1, - userStatus: user.state, - orgId: user.orgid || '' - }; - - try { - // Use the real response object if provided, otherwise use mock - const responseObj = res || mockRes; - - // Check if client prefers Bearer token authentication - const isWebClient = res?.req?.headers['origin'] || res?.req?.headers['referer']; - const explicitBearerRequest = res?.req?.headers['x-auth-method'] === 'bearer'; - - const prefersBearerAuth = res && !isWebClient && ( - res.req?.headers['authorization'] !== undefined || - explicitBearerRequest - ); - - let tokenPair: any; - - if (prefersBearerAuth && res) { - // Create token pair for Bearer authentication (no cookies) - tokenPair = this.jwtService.createTokenPair(tokenPayload); - } else { - // Cookie-based authentication (sets cookies automatically) - tokenPair = this.jwtService.create(tokenPayload, responseObj); - } - - // Check if user belongs to an organization and needs reauthentication - let requiresOrgReauth = false; - let orgLoginUrl: string | undefined; - let organizationName: string | undefined; - - if (user.orgid) { - const organization = await this.orgRepo.findById(user.orgid); - if (organization) { - organizationName = organization.name; - - // Check if user has logged in to organization within the last month - const oneMonthAgo = new Date(); - oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); - - const needsReauth = !user.Orglogindate || user.Orglogindate < oneMonthAgo; - - if (needsReauth && organization.url) { - requiresOrgReauth = true; - orgLoginUrl = organization.url; - - logAuth('User requires organization reauthentication', user.id, { - organizationId: user.orgid, - organizationName: organization.name, - lastOrgLogin: user.Orglogindate?.toISOString() || 'never', - orgLoginUrl: organization.url - }); - } - } - } - - logAuth('Login successful', user.id, { - authLevel: tokenPayload.authLevel, - userStatus: tokenPayload.userStatus, - orgId: tokenPayload.orgId, - requiresOrgReauth, - organizationName, - totalLoginTime: Date.now() - startTime - }); - let response: LoginResponse; - if (prefersBearerAuth){ - response = { - user: UserMapper.toShortDto(user), - token: tokenPair.accessToken, - refreshToken: tokenPair.refreshToken - }; - } - else { - response = { - user: UserMapper.toShortDto(user) - }; - } - - if (requiresOrgReauth) { - response.requiresOrgReauth = true; - response.orgLoginUrl = orgLoginUrl; - response.organizationName = organizationName; - } - - return response; - } catch (error) { - logError('Token creation failed during login', error as Error); - throw new Error('Login failed due to internal error'); - } - } catch (error) { - if (error instanceof Error) { - logError('Login handler error', error); - - // Handle database connection errors - if (error.message.includes('database connection')) { - logDatabase('Database connection error during login', undefined, Date.now() - startTime); - throw new Error('Database connection error'); - } - - // Re-throw authentication/validation errors as-is - if (error.message.includes('Invalid username') || - error.message.includes('Invalid password') || - error.message.includes('not verified') || - error.message.includes('deactivated') || - error.message === 'Login failed due to internal error' || - error.message === 'Database connection error') { - throw error; - } - } - // Default database error handling - logDatabase('Unexpected database error during login', undefined, Date.now() - startTime); - throw new Error('Database connection error'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/LogoutCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/LogoutCommandHandler.ts deleted file mode 100644 index ccbda81c..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/LogoutCommandHandler.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { Request, Response } from 'express'; -import { logAuth, logError, logWarning } from '../../Services/Logger'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { JWTService } from '../../Services/JWTService'; -import { RedisService } from '../../Services/RedisService'; - -export class LogoutCommandHandler { - private jwtService: JWTService; - private redisService: RedisService; - - constructor(private readonly userRepo: IUserRepository) { - this.jwtService = new JWTService(); - this.redisService = RedisService.getInstance(); - } - - async execute(userId: string, res: Response, req?: Request): Promise { - try { - logAuth('Logout process started', userId); - - // 1. Get tokens from request to blacklist them - let accessTokenToBlacklist: string | null = null; - let refreshTokenToBlacklist: string | null = null; - - if (req) { - // Extract access token from cookie or Authorization header - accessTokenToBlacklist = req.cookies['auth_token']; - if (!accessTokenToBlacklist && req.headers.authorization) { - const authHeader = req.headers.authorization; - if (authHeader.startsWith('Bearer ')) { - accessTokenToBlacklist = authHeader.substring(7); - } - } - - // Extract refresh token from cookie or header - refreshTokenToBlacklist = req.cookies['refresh_token']; - if (!refreshTokenToBlacklist) { - refreshTokenToBlacklist = req.headers['x-refresh-token'] as string; - } - } - - // 2. Blacklist both access and refresh tokens in Redis - if (accessTokenToBlacklist && req) { - try { - const decoded = this.jwtService.verify(req); - if (decoded && decoded.exp) { - const ttl = decoded.exp - Math.floor(Date.now() / 1000); - if (ttl > 0) { - await this.redisService.setWithExpiry(`blacklist:${accessTokenToBlacklist}`, 'true', ttl); - logAuth('Access token blacklisted', userId, { tokenExpiry: ttl }); - } - } - } catch (error) { - logWarning('Failed to blacklist access token', { userId, error: (error as Error).message }); - } - } - - // Blacklist refresh token if present - if (refreshTokenToBlacklist) { - try { - const refreshDecoded = this.jwtService.verifyRefreshToken(refreshTokenToBlacklist); - if (refreshDecoded && refreshDecoded.exp) { - const ttl = refreshDecoded.exp - Math.floor(Date.now() / 1000); - if (ttl > 0) { - await this.redisService.setWithExpiry(`blacklist:${refreshTokenToBlacklist}`, 'true', ttl); - logAuth('Refresh token blacklisted', userId, { tokenExpiry: ttl }); - } - } - } catch (error) { - logWarning('Failed to blacklist refresh token', { userId, error: (error as Error).message }); - } - } - - // 3. Use JWT service to clear cookies and set logout headers - if (req) { - this.jwtService.logout(req, res); - } - - // 4. Remove user from active sessions in Redis - try { - await this.redisService.removeActiveUser(userId); - logAuth('User removed from active sessions', userId); - } catch (error) { - logWarning('Failed to remove user from active sessions', { userId, error: (error as Error).message }); - // Continue even if this fails - } - - // 5. Update user's last logout timestamp in database - try { - const updateResult = await this.userRepo.update(userId, { updateDate: new Date() }); - if (updateResult) { - logAuth('User last logout timestamp updated', userId); - } - } catch (error) { - logWarning('Failed to update user logout timestamp', { userId, error: (error as Error).message }); - // Continue even if this fails - } - - // 6. Clear any user-specific cache entries - try { - // Clear user session data - await this.redisService.del(`user:${userId}:session`); - await this.redisService.del(`user:${userId}:active_chats`); - logAuth('User cache cleared', userId); - } catch (error) { - logWarning('Failed to clear user cache', { userId, error: (error as Error).message }); - // Continue even if this fails - } - - logAuth('User logout completed successfully', userId); - return true; - - } catch (error) { - logError('LogoutCommandHandler error', error as Error); - return false; - } - } - - /** - * Check if a token is blacklisted - */ - async isTokenBlacklisted(token: string): Promise { - try { - const result = await this.redisService.get(`blacklist:${token}`); - return result === 'true'; - } catch (error) { - logError('Error checking token blacklist', error as Error); - return false; - } - } - - /** - * Logout user from all devices by blacklisting all their active tokens - * This is a simplified version - in a real implementation you'd track active tokens per user - */ - async logoutFromAllDevices(userId: string): Promise { - try { - // Clear all user-related Redis keys - const userKeys = [ - `user:${userId}:session`, - `user:${userId}:active_chats`, - `user:${userId}:active_tokens`, - `user:${userId}:websocket_connections` - ]; - - for (const key of userKeys) { - try { - await this.redisService.del(key); - } catch (error) { - logWarning(`Failed to delete Redis key: ${key}`, { userId, error: (error as Error).message }); - } - } - - // Update user logout timestamp - await this.userRepo.update(userId, { updateDate: new Date() }); - - logAuth('User logged out from all devices', userId); - return true; - } catch (error) { - logError('Error logging out user from all devices', error as Error); - return false; - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommand.ts b/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommand.ts deleted file mode 100644 index b4c7b99c..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface RequestPasswordResetCommand { - language: 'hu' | 'de' | 'en'; - email: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommandHandler.ts deleted file mode 100644 index faba0fcd..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/RequestPasswordResetCommandHandler.ts +++ /dev/null @@ -1,70 +0,0 @@ - -import { RequestPasswordResetCommand } from './RequestPasswordResetCommand'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { EmailService } from '../../Services/EmailService'; -import { TokenService } from '../../Services/TokenService'; -import { logAuth, logWarning, logError } from '../../Services/Logger'; - -export class RequestPasswordResetCommandHandler { - constructor( - private userRepo: IUserRepository, - private emailService: EmailService - ) {} - - async execute(cmd: RequestPasswordResetCommand): Promise { - try { - if (!cmd.email) { - throw new Error('Email is required'); - } - - // Find user by email - const user = await this.userRepo.findByEmail(cmd.email); - - if (!user) { - // Don't reveal if user exists or not for security reasons - // Still return true but don't send email - logAuth(`Password reset requested for non-existent email: ${cmd.email}`); - return true; - } - - // Generate password reset token - const resetTokenData = TokenService.generatePasswordResetToken(); - - // Update user with reset token - user.token = await TokenService.hashToken(resetTokenData.token); - user.TokenExpires = resetTokenData.expiresAt; - - await this.userRepo.update(user.id, user); - - // Send password reset email - try { - const baseUrl = process.env.FRONTEND_URL || 'http://localhost:5173'; - const resetUrl = TokenService.generatePasswordResetUrl(baseUrl, resetTokenData.token); - - - const emailSent = await this.emailService.sendPasswordResetEmail( - user.email, - `${user.fname} ${user.lname}`, - resetTokenData.token, - resetUrl, - cmd.language - ); - - if (!emailSent) { - logWarning(`Failed to send password reset email to ${user.email}`); - // Don't throw error - request should still succeed even if email fails - } else { - logAuth(`Password reset email sent successfully to ${user.email}`); - } - } catch (emailError) { - logError('Error sending password reset email', emailError instanceof Error ? emailError : new Error(String(emailError))); - // Don't throw error - request should still succeed even if email fails - } - - return true; - } catch (error) { - logError('Password reset request error', error instanceof Error ? error : new Error(String(error))); - throw error; - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommand.ts b/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommand.ts deleted file mode 100644 index 31736ee1..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommand.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ResetPasswordCommand { - token: string; - newPassword: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommandHandler.ts deleted file mode 100644 index dad3b20e..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/ResetPasswordCommandHandler.ts +++ /dev/null @@ -1,58 +0,0 @@ - -import { ResetPasswordCommand } from './ResetPasswordCommand'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { TokenService } from '../../Services/TokenService'; -import { PasswordService } from '../../Services/PasswordService'; -import { logError } from '../../Services/Logger'; - -export class ResetPasswordCommandHandler { - constructor(private userRepo: IUserRepository) {} - - async execute(cmd: ResetPasswordCommand): Promise { - try { - if (!cmd.token) { - throw new Error('Reset token is required'); - } - - if (!cmd.newPassword) { - throw new Error('New password is required'); - } - - // Validate password strength - const validation = PasswordService.validatePasswordStrength(cmd.newPassword); - if (!validation.isValid) { - throw new Error(`Password validation failed: ${validation.errors.join(', ')}`); - } - - // Hash the token to compare with stored value - const hashedToken = await TokenService.hashToken(cmd.token); - - // Find user with this password reset token - const user = await this.userRepo.findByToken(hashedToken); - - if (!user) { - throw new Error('Invalid or expired reset token'); - } - - // Check if token is expired - if (user.TokenExpires && user.TokenExpires < new Date()) { - throw new Error('Reset token has expired'); - } - - // Hash the new password - const hashedPassword = await PasswordService.hashPassword(cmd.newPassword); - - // Update user password and clear reset token - user.password = hashedPassword; - user.token = null; - user.TokenExpires = null; - - await this.userRepo.update(user.id, user); - - return true; - } catch (error) { - logError('Password reset error', error instanceof Error ? error : new Error(String(error))); - throw error; - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommand.ts b/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommand.ts deleted file mode 100644 index 76c5f2ca..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommand.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface UpdateUserCommand { - id: string; - orgid?: string; - username?: string; - password?: string; - email?: string; - fname?: string; - lname?: string; - code?: string; - phone?: string; - state?: number; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommandHandler.ts deleted file mode 100644 index 9f348592..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/UpdateUserCommandHandler.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { UpdateUserCommand } from './UpdateUserCommand'; - -import { ShortUserDto } from '../../DTOs/UserDto'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { PasswordService } from '../../Services/PasswordService'; - -export class UpdateUserCommandHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(cmd: UpdateUserCommand): Promise { - const updateData = { ...cmd }; - - // Hash the password if it's being updated - if (cmd.password) { - // Validate password strength - const passwordValidation = PasswordService.validatePasswordStrength(cmd.password); - if (!passwordValidation.isValid) { - throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`); - } - - updateData.password = await PasswordService.hashPassword(cmd.password); - } - - const updated = await this.userRepo.update(cmd.id, updateData); - if (!updated) return null; - return UserMapper.toShortDto(updated); - } -} diff --git a/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommand.ts b/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommand.ts deleted file mode 100644 index dd3e7e1f..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommand.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface VerifyEmailCommand { - token: string; -} diff --git a/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommandHandler.ts b/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommandHandler.ts deleted file mode 100644 index 44c3f95a..00000000 --- a/SerpentRace_Backend/src/Application/User/commands/VerifyEmailCommandHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ - -import { VerifyEmailCommand } from './VerifyEmailCommand'; -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { TokenService } from '../../Services/TokenService'; -import { UserState } from '../../../Domain/User/UserAggregate'; -import { logError } from '../../Services/Logger'; - -export class VerifyEmailCommandHandler { - constructor(private userRepo: IUserRepository) {} - - async execute(cmd: VerifyEmailCommand): Promise { - try { - if (!cmd.token) { - throw new Error('Verification token is required'); - } - - // Hash the token to compare with stored value - const hashedToken = await TokenService.hashToken(cmd.token); - - // Find user with this verification token - const user = await this.userRepo.findByToken(hashedToken); - - if (!user) { - throw new Error('Invalid or expired verification token'); - } - - // Check if token is expired - if (user.TokenExpires && user.TokenExpires < new Date()) { - throw new Error('Verification token has expired'); - } - - // Update user verification status - user.token = null; - user.TokenExpires = null; - user.state = UserState.VERIFIED_REGULAR; - - await this.userRepo.update(user.id, user); - - return true; - } catch (error) { - logError('Email verification error', error instanceof Error ? error : new Error(String(error))); - throw error; - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQuery.ts b/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQuery.ts deleted file mode 100644 index 21b92e4c..00000000 --- a/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQuery.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GetUserByIdQuery { - id: string; -} diff --git a/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQueryHandler.ts b/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQueryHandler.ts deleted file mode 100644 index ae8ff109..00000000 --- a/SerpentRace_Backend/src/Application/User/queries/GetUserByIdQueryHandler.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { GetUserByIdQuery } from './GetUserByIdQuery'; -import { DetailUserDto } from '../../DTOs/UserDto'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { logError } from '../../Services/Logger'; - -export class GetUserByIdQueryHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(query: GetUserByIdQuery): Promise { - try { - const user = await this.userRepo.findById(query.id); - if (!user) return null; - return UserMapper.toDetailDto(user); - } catch (error) { - logError('GetUserByIdQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Handle invalid ID format - if (error instanceof Error && error.message.includes('invalid') && error.message.includes('uuid')) { - return null; // Treat invalid UUID as not found - } - - // Handle database errors - if (error instanceof Error && error.message.includes('database')) { - throw new Error('Database connection error'); - } - - // Generic error for other cases - throw new Error('Failed to retrieve user'); - } - } -} diff --git a/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQuery.ts b/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQuery.ts deleted file mode 100644 index b1fc4346..00000000 --- a/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQuery.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GetUsersByPageQuery { - from: number; - to: number; - includeDeleted?: boolean; -} diff --git a/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQueryHandler.ts b/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQueryHandler.ts deleted file mode 100644 index 0c8c33ae..00000000 --- a/SerpentRace_Backend/src/Application/User/queries/GetUsersByPageQueryHandler.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { IUserRepository } from '../../../Domain/IRepository/IUserRepository'; -import { GetUsersByPageQuery } from './GetUsersByPageQuery'; -import { ShortUserDto } from '../../DTOs/UserDto'; -import { UserMapper } from '../../DTOs/Mappers/UserMapper'; -import { logError, logRequest } from '../../Services/Logger'; - -export class GetUsersByPageQueryHandler { - constructor(private readonly userRepo: IUserRepository) {} - - async execute(query: GetUsersByPageQuery): Promise<{ users: ShortUserDto[], totalCount: number }> { - try { - // Validate pagination parameters - if (query.from < 0 || query.to < query.from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = query.to - query.from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - logRequest('Get users by page query started', undefined, undefined, { - from: query.from, - to: query.to, - includeDeleted: query.includeDeleted || false - }); - - const result = query.includeDeleted - ? await this.userRepo.findByPageIncludingDeleted(query.from, query.to) - : await this.userRepo.findByPage(query.from, query.to); - - logRequest('Get users by page query completed', undefined, undefined, { - from: query.from, - to: query.to, - returned: result.users.length, - totalCount: result.totalCount, - includeDeleted: query.includeDeleted || false - }); - - return { - users: UserMapper.toShortDtoList(result.users), - totalCount: result.totalCount - }; - } catch (error) { - logError('GetUsersByPageQueryHandler error', error instanceof Error ? error : new Error(String(error))); - - // Handle database errors - if (error instanceof Error && error.message.includes('database')) { - throw new Error('Database connection error'); - } - - // Re-throw validation errors as-is - if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) { - throw error; - } - - throw new Error('Failed to retrieve users'); - } - } -} diff --git a/SerpentRace_Backend/src/Domain/Chat/ChatAggregate.ts b/SerpentRace_Backend/src/Domain/Chat/ChatAggregate.ts deleted file mode 100644 index 664a743a..00000000 --- a/SerpentRace_Backend/src/Domain/Chat/ChatAggregate.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, UpdateDateColumn, CreateDateColumn } from 'typeorm'; - -export interface Message { - id: string; // UUID for each message - date: Date; - userid: string; // UUID reference to UserAggregate - text: string; -} - -export const ChatState = { - ACTIVE: 0, - ARCHIVE: 1, - SOFT_DELETE: 2 -} as const; - -export type ChatStateType = typeof ChatState[keyof typeof ChatState]; - -export const ChatType = { - DIRECT: 'direct', - GROUP: 'group', - GAME: 'game' -} as const; - -export type ChatTypeType = typeof ChatType[keyof typeof ChatType]; - -@Entity('Chats') -export class ChatAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 50, default: ChatType.DIRECT }) - type!: ChatTypeType; - - @Column({ type: 'varchar', length: 255, nullable: true }) - name!: string | null; // Group name or Game name - - @Column({ type: 'uuid', nullable: true }) - gameId!: string | null; // Game UUID reference for game chats - - @Column({ type: 'uuid', nullable: true }) - createdBy!: string | null; // User who created the group/chat - - @Column('uuid', { array: true }) - users!: string[]; // Active participants - - @Column('json', { default: [] }) - messages!: Message[]; // Active messages (last 10 per user, max 2 weeks) - - @Column({ type: 'timestamp', nullable: true }) - lastActivity!: Date | null; - - @CreateDateColumn() - createDate!: Date; - - @UpdateDateColumn() - updateDate!: Date; - - @Column({ type: 'int', default: ChatState.ACTIVE }) - state!: ChatStateType; - - // Archive when inactive for specified period - @Column({ type: 'timestamp', nullable: true }) - archiveDate!: Date | null; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/Chat/ChatArchiveAggregate.ts b/SerpentRace_Backend/src/Domain/Chat/ChatArchiveAggregate.ts deleted file mode 100644 index 518042a1..00000000 --- a/SerpentRace_Backend/src/Domain/Chat/ChatArchiveAggregate.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; -import { Message } from './ChatAggregate'; - -@Entity('ChatArchives') -export class ChatArchiveAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'uuid' }) - chatId!: string; // Reference to original chat - - @Column('json') - archivedMessages!: Message[]; // All archived messages - - @Column({ type: 'timestamp' }) - archivedAt!: Date; - - @CreateDateColumn() - createDate!: Date; - - // Metadata for context - @Column({ type: 'varchar', length: 50 }) - chatType!: string; // direct, group, game - - @Column({ type: 'varchar', length: 255, nullable: true }) - chatName!: string | null; - - @Column({ type: 'uuid', nullable: true }) - gameId!: string | null; - - @Column('uuid', { array: true }) - participants!: string[]; // Users who participated -} diff --git a/SerpentRace_Backend/src/Domain/Contact/ContactAggregate.ts b/SerpentRace_Backend/src/Domain/Contact/ContactAggregate.ts deleted file mode 100644 index a7e79581..00000000 --- a/SerpentRace_Backend/src/Domain/Contact/ContactAggregate.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; - -export enum ContactType { - BUG = 0, - PROBLEM = 1, - QUESTION = 2, - SALES = 3, - OTHER = 4 -} - -export enum ContactState { - ACTIVE = 0, - RESOLVED = 1, - SOFT_DELETE = 2 -} - -@Entity('Contacts') -export class ContactAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 255 }) - name!: string; - - @Column({ type: 'varchar', length: 255 }) - email!: string; - - @Column({ type: 'uuid', nullable: true }) - userid!: string | null; // If logged in user - - @Column({ type: 'int' }) - type!: ContactType; - - @Column({ type: 'text' }) - txt!: string; - - @Column({ type: 'int', default: ContactState.ACTIVE }) - state!: ContactState; - - @CreateDateColumn() - createDate!: Date; - - @UpdateDateColumn() - updateDate!: Date; - - // Admin response field for email response feature - @Column({ type: 'text', nullable: true }) - adminResponse!: string | null; - - @Column({ type: 'timestamp', nullable: true }) - responseDate!: Date | null; - - @Column({ type: 'uuid', nullable: true }) - respondedBy!: string | null; // Admin user id who responded -} diff --git a/SerpentRace_Backend/src/Domain/Deck/DeckAggregate.ts b/SerpentRace_Backend/src/Domain/Deck/DeckAggregate.ts deleted file mode 100644 index a9b7faec..00000000 --- a/SerpentRace_Backend/src/Domain/Deck/DeckAggregate.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; -import { OrganizationAggregate } from '../Organization/OrganizationAggregate'; -import { UserAggregate } from '../User/UserAggregate'; -import { logError } from '../../Application/Services/Logger'; - -export enum Type { - LUCK = 0, - JOKER = 1, - QUESTION = 2 -} - -export enum CType { - PUBLIC = 0, - PRIVATE = 1, - ORGANIZATION = 2 -} - -export enum State { - ACTIVE = 0, - SOFT_DELETE = 1 -} - -export enum CardType { - QUIZ = 0, - SENTENCE_PAIRING = 1, - OWN_ANSWER = 2, - TRUE_FALSE = 3, - CLOSER = 4 -} - -export enum ConsequenceType { - MOVE_FORWARD = 0, - MOVE_BACKWARD = 1, - LOSE_TURN = 2, - EXTRA_TURN = 3, - GO_TO_START = 5 -} - -export interface Consequence { - type: ConsequenceType; - value?: number; -} - -export interface Card { - text: string; - type?: CardType; - answer?: string | null; - consequence?: Consequence | null; -} - -@Entity('Decks') -export class DeckAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 255 }) - name!: string; - - @Column({ type: 'int'}) - type!: Type; - - @Column({ type: 'uuid', name: 'user_id' }) - userid!: string; - - @CreateDateColumn({ name: 'creation_date' }) - creationdate!: Date; - - @Column({ type: 'json' }) - cards!: Card[]; - - @Column({ type: 'int', default: 0, name: 'played_number' }) - playedNumber!: number; - - @Column({ type: 'int', default: CType.PUBLIC }) - ctype!: CType; - - @UpdateDateColumn() - updateDate!: Date; - - @Column({ type: 'int', default: State.ACTIVE }) - state!: State; - - @ManyToOne(() => OrganizationAggregate, { nullable: true }) - @JoinColumn({ name: 'organization_id' }) - organization!: OrganizationAggregate | null; - - @ManyToOne(() => UserAggregate, { eager: false }) - @JoinColumn({ name: 'user_id' }) - user!: UserAggregate | null; - - isEditable(userId:string): boolean{ - // A deck is editable if the user is the creator - if (!this.user) { - logError(`DeckAggregate.isEditable: User is null for deck id ${this.id}`); - return false; - } - //if admin, always editable - if (this.user?.isAdmin) { - return true; - } - return this.user?.id.toString() === userId;; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/Game/GameAggregate.ts b/SerpentRace_Backend/src/Domain/Game/GameAggregate.ts deleted file mode 100644 index 46ddf85e..00000000 --- a/SerpentRace_Backend/src/Domain/Game/GameAggregate.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { Consequence, CardType } from '../Deck/DeckAggregate'; -import { UserAggregate } from '../User/UserAggregate'; -import { OrganizationAggregate } from '../Organization/OrganizationAggregate'; - -export enum GameState { - WAITING = 0, - ACTIVE = 1, - FINISHED = 2, - CANCELLED = 3 -} - -export enum LoginType { - PUBLIC = 0, - PRIVATE = 1, - ORGANIZATION = 2 -} - -export enum DeckType { - JOCKER = 0, - LUCK = 1, - QUEST = 2 -} - -export interface GameCard { - cardid: string; - question?: string; - answer?: any; // Support complex answer structures (string, object, array) - type?: CardType; // Card type for validation logic - consequence?: Consequence | null; - played?: boolean; - playerid?: string; -} - -export interface GameDeck { - deckid: string; - decktype: DeckType; - cards: GameCard[]; -} - -@Entity('Games') -export class GameAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 10, unique: true }) - gamecode!: string; - - @Column({ type: 'int' }) - maxplayers!: number; - - @Column({ type: 'int', default: LoginType.PUBLIC }) - logintype!: LoginType; - - @Column({ type: 'int', default: 50 }) - boardsize!: number; - - @Column({ type: 'uuid', nullable: false, name: 'createdBy' }) - createdby!: string; - - @Column({ type: 'uuid', nullable: true, name: 'organizationid' }) - orgid!: string | null; - - @Column({ type: 'jsonb', default: () => "'[]'", name: 'decks' }) - gamedecks!: GameDeck[]; - - @Column({ type: 'uuid', array: true, default: () => "'{}'", name: 'playerids' }) - players!: string[]; - - @Column({ type: 'uuid', nullable: true, name: 'winnerId' }) - winnerId!: string | null; - - @Column({ type: 'int', default: GameState.WAITING }) - state!: GameState; - - @CreateDateColumn({ name: 'createDate' }) - createdate!: Date; - - @Column({ type: 'timestamp', nullable: true, name: 'start_date' }) - startdate!: Date | null; - - @Column({ type: 'timestamp', nullable: true, name: 'finishDate' }) - enddate!: Date | null; - - @UpdateDateColumn({ name: 'updateDate' }) - updateDate!: Date; - - @ManyToOne(() => UserAggregate, { eager: false }) - @JoinColumn({ name: 'createdBy' }) - user!: UserAggregate | null; - - @ManyToOne(() => UserAggregate, { eager: false }) - @JoinColumn({ name: 'winnerId' }) - winner!: UserAggregate | null; - - @ManyToOne(() => OrganizationAggregate, { eager: false }) - @JoinColumn({ name: 'organizationId' }) - organization!: OrganizationAggregate | null; -} - -// Board Generation Types -export interface GameField { - position: number; - type: 'regular' | 'positive' | 'negative' | 'luck'; - stepValue?: number; -} - -export interface BoardData { - gameId?: string; - fields: GameField[]; - generationComplete?: boolean; - generatedAt?: Date; - error?: string; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/Game/GameSnapshotAggregate.ts b/SerpentRace_Backend/src/Domain/Game/GameSnapshotAggregate.ts deleted file mode 100644 index 0f83f29b..00000000 --- a/SerpentRace_Backend/src/Domain/Game/GameSnapshotAggregate.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; -import { GameAggregate } from './GameAggregate'; - -export interface PlayerSnapshot { - playerId: string; - playerName: string; - boardPosition: number; - extraTurns: number; - turnsToLose: number; - isOnline: boolean; -} - -export interface GameStateSnapshot { - currentPlayer: string; - currentPlayerName: string; - turnNumber: number; - turnOrder: string[]; - playerPositions: PlayerSnapshot[]; - boardFields?: any[]; - deckStates?: any; - pendingActions?: any; -} - -export enum SnapshotTrigger { - TURN_INTERVAL = 'turn_interval', // Every N turns - PLAYER_DISCONNECT = 'player_disconnect', // When player disconnects - CRITICAL_EVENT = 'critical_event', // Important game events - MANUAL = 'manual', // Manual checkpoint - SERVER_SHUTDOWN = 'server_shutdown' // Before server shutdown -} - -@Entity('GameSnapshots') -@Index(['gameid', 'createdat']) -@Index(['gameid', 'trigger']) -export class GameSnapshotAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'uuid', name: 'gameid' }) - gameid!: string; - - @Column({ type: 'int', name: 'turn_number' }) - turnNumber!: number; - - @Column({ - type: 'enum', - enum: SnapshotTrigger, - name: 'trigger' - }) - trigger!: SnapshotTrigger; - - @Column({ type: 'jsonb', name: 'game_state' }) - gameState!: GameStateSnapshot; - - @Column({ type: 'jsonb', name: 'redis_state', nullable: true }) - redisState!: any | null; - - @Column({ type: 'text', name: 'notes', nullable: true }) - notes!: string | null; - - @CreateDateColumn({ name: 'createdat' }) - createdat!: Date; - - @ManyToOne(() => GameAggregate, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'gameid' }) - game?: GameAggregate; -} diff --git a/SerpentRace_Backend/src/Domain/Game/TurnHistoryAggregate.ts b/SerpentRace_Backend/src/Domain/Game/TurnHistoryAggregate.ts deleted file mode 100644 index d9c62804..00000000 --- a/SerpentRace_Backend/src/Domain/Game/TurnHistoryAggregate.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; -import { GameAggregate } from './GameAggregate'; - -export enum TurnActionType { - DICE_ROLL = 'dice_roll', - CARD_DRAWN = 'card_drawn', - ANSWER_SUBMITTED = 'answer_submitted', - POSITION_GUESS = 'position_guess', - GAMEMASTER_DECISION = 'gamemaster_decision', - LUCK_CONSEQUENCE = 'luck_consequence', - EXTRA_TURN = 'extra_turn', - TURN_LOST = 'turn_lost', - PLAYER_DISCONNECTED = 'player_disconnected', - TIMEOUT = 'timeout' -} - -export interface TurnActionData { - diceValue?: number; - cardId?: string; - cardType?: string; - question?: string; - answer?: any; - isCorrect?: boolean; - guessedPosition?: number; - actualPosition?: number; - consequenceType?: string; - consequenceValue?: number; - decision?: string; - reason?: string; - [key: string]: any; // Allow additional properties -} - -@Entity('TurnHistory') -@Index(['gameid', 'turnNumber']) -@Index(['gameid', 'playerid']) -@Index(['createdat']) -export class TurnHistoryAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'uuid', name: 'gameid' }) - gameid!: string; - - @Column({ type: 'uuid', name: 'playerid' }) - playerid!: string; - - @Column({ type: 'varchar', length: 255, name: 'playername' }) - playername!: string; - - @Column({ type: 'int', name: 'turn_number' }) - turnNumber!: number; - - @Column({ - type: 'enum', - enum: TurnActionType, - name: 'action_type' - }) - actionType!: TurnActionType; - - @Column({ type: 'jsonb', name: 'action_data', nullable: true }) - actionData!: TurnActionData | null; - - @Column({ type: 'int', name: 'position_before' }) - positionBefore!: number; - - @Column({ type: 'int', name: 'position_after' }) - positionAfter!: number; - - @CreateDateColumn({ name: 'createdat' }) - createdat!: Date; - - @ManyToOne(() => GameAggregate, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'gameid' }) - game?: GameAggregate; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IBaseRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IBaseRepository.ts deleted file mode 100644 index 4a06282d..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IBaseRepository.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Base Repository Interface - * Contains common repository methods that all repositories should implement - * Reduces code duplication across repository interfaces - */ -export interface IBaseRepository { - // Core CRUD operations - create(entity: Partial): Promise; - findById(id: string): Promise; - findByIdIncludingDeleted(id: string): Promise; - update(id: string, update: Partial): Promise; - delete(id: string): Promise; - softDelete(id: string): Promise; -} - -/** - * Paginated Repository Interface - * For repositories that support pagination and search operations - * This allows typed responses for each repository type - */ -export interface IPaginatedRepository extends IBaseRepository { - // Pagination operations - findByPage(from: number, to: number): Promise; - findByPageIncludingDeleted(from: number, to: number): Promise; - - // Search operations - search(query: string, limit?: number, offset?: number): Promise; - searchIncludingDeleted(query: string, limit?: number, offset?: number): Promise; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/IRepository/IChatArchiveRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IChatArchiveRepository.ts deleted file mode 100644 index 494197d4..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IChatArchiveRepository.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ChatArchiveAggregate } from '../Chat/ChatArchiveAggregate'; - -export interface IChatArchiveRepository { - create(archive: Partial): Promise; - findAll(): Promise; - findById(id: string): Promise; - findByChatId(chatId: string): Promise; - findByGameId(gameId: string): Promise; - delete(id: string): Promise; - cleanup(olderThanDays: number): Promise; // Clean up old archives -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IChatRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IChatRepository.ts deleted file mode 100644 index 95f3c6f9..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IChatRepository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ChatAggregate } from '../Chat/ChatAggregate'; -import { ChatArchiveAggregate } from '../Chat/ChatArchiveAggregate'; -import { IBaseRepository } from './IBaseRepository'; - -export interface IChatRepository extends IBaseRepository { - // Pagination operations with proper typing - findByPage(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }>; - findByPageIncludingDeleted(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }>; - - // Chat-specific methods - findByUserId(userId: string): Promise; - findByUserIdIncludingDeleted(userId: string): Promise; - findByGameId(gameId: string): Promise; - findActiveChatsForUser(userId: string): Promise; - findInactiveChats(inactivityMinutes: number): Promise; - archiveChat(chat: ChatAggregate): Promise; - getArchivedChat(chatId: string): Promise; - restoreFromArchive(chatId: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IContactRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IContactRepository.ts deleted file mode 100644 index a055d2f6..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IContactRepository.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ContactAggregate } from '../Contact/ContactAggregate'; -import { IBaseRepository } from './IBaseRepository'; - -export interface IContactRepository extends IBaseRepository { - // Pagination operations with proper typing - findByPage(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }>; - findByPageIncludingDeleted(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }>; - - // Contact-specific search methods (different signature than base) - search(searchTerm: string): Promise; - searchIncludingDeleted(searchTerm: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IDeckRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IDeckRepository.ts deleted file mode 100644 index a858f5fe..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IDeckRepository.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DeckAggregate } from '../Deck/DeckAggregate'; -import { IPaginatedRepository } from './IBaseRepository'; - -export interface IDeckRepository extends IPaginatedRepository { - // Deck-specific methods for restrictions and filtering - countActiveByUserId(userId: string): Promise; - countOrganizationalByUserId(userId: string): Promise; - findFilteredDecks(userId: string, userOrgId?: string | null, isAdmin?: boolean, from?: number, to?: number): Promise<{ decks: DeckAggregate[], totalCount: number }>; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IGameRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IGameRepository.ts deleted file mode 100644 index 73d9b958..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IGameRepository.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { GameAggregate, GameState } from '../Game/GameAggregate'; -import { IPaginatedRepository } from './IBaseRepository'; - -export interface IGameRepository extends IPaginatedRepository { - // Game-specific methods - findByGameCode(gamecode: string): Promise; - findActiveGames(): Promise; - findGamesByPlayer(playerId: string): Promise; - findWaitingGames(): Promise; - findFinishedGames(from?: number, to?: number): Promise<{ games: GameAggregate[], totalCount: number }>; - addPlayerToGame(gameId: string, playerId: string): Promise; - removePlayerFromGame(gameId: string, playerId: string): Promise; - updateGameState(gameId: string, state: GameState, winner?: string): Promise; -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/IRepository/IGameSnapshotRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IGameSnapshotRepository.ts deleted file mode 100644 index 0a97c08a..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IGameSnapshotRepository.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { GameSnapshotAggregate, SnapshotTrigger } from '../Game/GameSnapshotAggregate'; - -export interface IGameSnapshotRepository { - /** - * Save a game state snapshot - */ - save(snapshot: GameSnapshotAggregate): Promise; - - /** - * Get the most recent snapshot for a game - */ - findLatestByGameId(gameId: string): Promise; - - /** - * Get all snapshots for a game - */ - findByGameId(gameId: string): Promise; - - /** - * Get snapshots by trigger type - */ - findByGameAndTrigger(gameId: string, trigger: SnapshotTrigger): Promise; - - /** - * Get snapshot at specific turn - */ - findByGameAndTurn(gameId: string, turnNumber: number): Promise; - - /** - * Delete old snapshots (keep only last N) - */ - deleteOldSnapshots(gameId: string, keepCount: number): Promise; - - /** - * Delete all snapshots for a game - */ - deleteByGameId(gameId: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IOrganizationRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IOrganizationRepository.ts deleted file mode 100644 index 373d3fb6..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IOrganizationRepository.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { OrganizationAggregate } from '../Organization/OrganizationAggregate'; -import { IPaginatedRepository } from './IBaseRepository'; - -export interface IOrganizationRepository extends IPaginatedRepository { - // Organization-specific methods can be added here if needed -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/ITurnHistoryRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/ITurnHistoryRepository.ts deleted file mode 100644 index 653b3679..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/ITurnHistoryRepository.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { TurnHistoryAggregate, TurnActionType, TurnActionData } from '../Game/TurnHistoryAggregate'; - -export interface ITurnHistoryRepository { - /** - * Save a turn history entry - */ - save(turnHistory: TurnHistoryAggregate): Promise; - - /** - * Get all turn history for a game - */ - findByGameId(gameId: string): Promise; - - /** - * Get turn history for a specific player in a game - */ - findByGameAndPlayer(gameId: string, playerId: string): Promise; - - /** - * Get the last N turns for a game - */ - findLastNTurns(gameId: string, limit: number): Promise; - - /** - * Get turn count for a game - */ - countTurnsByGame(gameId: string): Promise; - - /** - * Delete all turn history for a game - */ - deleteByGameId(gameId: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/IRepository/IUserRepository.ts b/SerpentRace_Backend/src/Domain/IRepository/IUserRepository.ts deleted file mode 100644 index 4db923b0..00000000 --- a/SerpentRace_Backend/src/Domain/IRepository/IUserRepository.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { UserAggregate } from '../User/UserAggregate'; -import { IPaginatedRepository } from './IBaseRepository'; - -export interface IUserRepository extends IPaginatedRepository { - // User-specific methods - findByUsername(username: string): Promise; - findByEmail(email: string): Promise; - findByToken(token: string): Promise; - deactivate(id: string): Promise; - activate(id: string): Promise; -} diff --git a/SerpentRace_Backend/src/Domain/Organization/OrganizationAggregate.ts b/SerpentRace_Backend/src/Domain/Organization/OrganizationAggregate.ts deleted file mode 100644 index 5a3be365..00000000 --- a/SerpentRace_Backend/src/Domain/Organization/OrganizationAggregate.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; -import { UserAggregate } from '../User/UserAggregate'; - -export const OrganizationState = { - REGISTERED: 0, - ACTIVE: 1, - SOFT_DELETE: 2 -} as const; - -export type OrganizationStateType = typeof OrganizationState[keyof typeof OrganizationState]; - -@Entity('Organizations') -export class OrganizationAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'varchar', length: 255 }) - name!: string; - - @Column({ type: 'varchar', length: 100 }) - contactfname!: string; - - @Column({ type: 'varchar', length: 100 }) - contactlname!: string; - - @Column({ type: 'varchar', length: 20 }) - contactphone!: string; - - @Column({ type: 'varchar', length: 255 }) - contactemail!: string; - - @Column({ type: 'int', default: OrganizationState.REGISTERED }) - state!: OrganizationStateType; - - @CreateDateColumn() - regdate!: Date; - - @UpdateDateColumn({ name: 'updateDate' }) - updateDate!: Date; - - @Column({ type: 'varchar', length: 500, nullable: true }) - url!: string | null; - - @Column({ type: 'int', default: 0 }) - userinorg!: number; - - @Column({ type: 'int', nullable: true }) - maxOrganizationalDecks!: number | null; - - @OneToMany(() => UserAggregate, user => user.orgid) - users!: UserAggregate[]; - } \ No newline at end of file diff --git a/SerpentRace_Backend/src/Domain/User/UserAggregate.ts b/SerpentRace_Backend/src/Domain/User/UserAggregate.ts deleted file mode 100644 index f67fefcf..00000000 --- a/SerpentRace_Backend/src/Domain/User/UserAggregate.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; - -export enum UserState { - REGISTERED_NOT_VERIFIED = 0, - VERIFIED_REGULAR = 1, - VERIFIED_PREMIUM = 2, - SOFT_DELETE = 3, - DEACTIVATED = 4, - ADMIN = 5 -} - -@Entity('Users') -export class UserAggregate { - @PrimaryGeneratedColumn('uuid') - id!: string; - - @Column({ type: 'uuid', nullable: true }) - orgid!: string | null; - - @Column({ type: 'varchar', length: 100, unique: true }) - username!: string; - - @Column({ type: 'varchar', length: 255 }) - password!: string; - - @Column({ type: 'varchar', length: 255, unique: true }) - email!: string; - - @Column({ type: 'varchar', length: 100 }) - fname!: string; - - @Column({ type: 'varchar', length: 100 }) - lname!: string; - - @Column({ type: 'varchar', length: 255, nullable: true }) - token!: string | null; - - @Column({ type: 'timestamp', nullable: true }) - TokenExpires!: Date | null; - - @Column({ type: 'varchar', length: 20, nullable: true }) - phone!: string | null; - - @Column({ - type: 'int', - default: UserState.REGISTERED_NOT_VERIFIED - }) - state!: UserState; - - @CreateDateColumn() - regdate!: Date; - - @UpdateDateColumn() - updateDate!: Date; - - @Column({ type: 'timestamp', nullable: true }) - Orglogindate!: Date | null; - - get isAdmin(): boolean { - return this.state === UserState.ADMIN; - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Infrastructure/Migrations/1762370334693-full.ts b/SerpentRace_Backend/src/Infrastructure/Migrations/1762370334693-full.ts deleted file mode 100644 index 610a6821..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Migrations/1762370334693-full.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Full1762370334693 implements MigrationInterface { - name = 'Full1762370334693' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "Games" RENAME COLUMN "winnerid" TO "winnerId"`); - await queryRunner.query(`ALTER TABLE "Games" ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId") REFERENCES "Users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "Games" DROP CONSTRAINT "FK_330362bff8b25bb573f31fb4023"`); - await queryRunner.query(`ALTER TABLE "Games" RENAME COLUMN "winnerId" TO "winnerid"`); - } - -} diff --git a/SerpentRace_Backend/src/Infrastructure/Migrationsettings/1762370333970-full.ts b/SerpentRace_Backend/src/Infrastructure/Migrationsettings/1762370333970-full.ts deleted file mode 100644 index aeb7547a..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Migrationsettings/1762370333970-full.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Full1762370333970 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - } - - public async down(queryRunner: QueryRunner): Promise { - } - -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/ChatArchiveRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/ChatArchiveRepository.ts deleted file mode 100644 index 3ec25839..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/ChatArchiveRepository.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Repository } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { ChatArchiveAggregate } from '../../Domain/Chat/ChatArchiveAggregate'; -import { IChatArchiveRepository } from '../../Domain/IRepository/IChatArchiveRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; -import { ChatState } from '../../Domain/Chat/ChatAggregate'; - -export class ChatArchiveRepository implements IChatArchiveRepository { - private repo: Repository; - - constructor() { - this.repo = AppDataSource.getRepository(ChatArchiveAggregate); - } - - async create(archive: Partial) { - const startTime = Date.now(); - try { - const result = await this.repo.save(archive); - logDatabase('Chat archive created successfully', undefined, Date.now() - startTime, { - archiveId: result.id, - chatId: result.chatId, - messageCount: result.archivedMessages?.length || 0 - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.create error', error as Error); - throw new Error('Failed to create chat archive in database'); - } - } - - async findAll() { - const startTime = Date.now(); - try { - const result = await this.repo.find(); - logDatabase('All chat archives retrieved', undefined, Date.now() - startTime, { - count: result.length - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.findAll error', error as Error); - throw new Error('Failed to retrieve chat archives from database'); - } - } - - async findById(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ id }); - logDatabase('Chat archive retrieved by id', `findById(${id})`, Date.now() - startTime, { - archiveId: id, - found: !!result - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.findById error', error as Error); - throw new Error('Failed to find chat archive by id'); - } - } - - async findByChatId(chatId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .find({ - where: { chatId }, - order: { archivedAt: 'DESC' } - }); - - logDatabase('Chat archives retrieved by chat id', `findByChatId(${chatId})`, Date.now() - startTime, { - chatId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.findByChatId error', error as Error); - throw new Error('Failed to find chat archives by chat id'); - } - } - - async findByGameId(gameId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .find({ - where: { gameId }, - order: { archivedAt: 'DESC' } - }); - - logDatabase('Chat archives retrieved by game id', `findByGameId(${gameId})`, Date.now() - startTime, { - gameId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.findByGameId error', error as Error); - throw new Error('Failed to find chat archives by game id'); - } - } - - async delete(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.delete(id); - logDatabase('Chat archive deleted', `delete(${id})`, Date.now() - startTime, { - archiveId: id, - affected: result.affected - }); - return result; - } catch (error) { - logError('ChatArchiveRepository.delete error', error as Error); - throw new Error('Failed to delete chat archive'); - } - } - - async cleanup(olderThanDays: number) { - const startTime = Date.now(); - try { - const cutoffDate = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000); - - const result = await this.repo - .createQueryBuilder() - .delete() - .where('archivedAt < :cutoffDate', { cutoffDate }) - .execute(); - - logDatabase('Chat archive cleanup completed', `cleanup(${olderThanDays} days)`, Date.now() - startTime, { - olderThanDays, - deleted: result.affected, - cutoffDate - }); - - return result.affected || 0; - } catch (error) { - logError('ChatArchiveRepository.cleanup error', error as Error); - throw new Error('Failed to cleanup old chat archives'); - } - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/ChatRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/ChatRepository.ts deleted file mode 100644 index 1b266e7f..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/ChatRepository.ts +++ /dev/null @@ -1,358 +0,0 @@ -import { Repository, MoreThan, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { ChatAggregate, ChatState, ChatType } from '../../Domain/Chat/ChatAggregate'; -import { ChatArchiveAggregate } from '../../Domain/Chat/ChatArchiveAggregate'; -import { IChatRepository } from '../../Domain/IRepository/IChatRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class ChatRepository implements IChatRepository { - private repo: Repository; - private archiveRepo: Repository; - - constructor() { - this.repo = AppDataSource.getRepository(ChatAggregate); - this.archiveRepo = AppDataSource.getRepository(ChatArchiveAggregate); - } - - async create(chat: Partial) { - const startTime = Date.now(); - try { - const result = await this.repo.save(chat); - logDatabase('Chat created successfully', undefined, Date.now() - startTime, { - chatId: result.id, - type: result.type, - participants: result.users?.length || 0 - }); - return result; - } catch (error) { - logError('ChatRepository.create error', error as Error); - throw new Error('Failed to create chat in database'); - } - } - - async findByPage(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const skip = from; - const take = to - from + 1; - - const [chats, totalCount] = await this.repo.findAndCount({ - where: { state: Not(ChatState.SOFT_DELETE) }, - order: { createDate: 'DESC' }, - skip, - take - }); - - logDatabase('Chats page retrieved successfully', undefined, Date.now() - startTime, { - from, - to, - returned: chats.length, - totalCount - }); - - return { chats, totalCount }; - } catch (error) { - logError('ChatRepository.findByPage error', error as Error); - throw new Error('Failed to retrieve chats page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const skip = from; - const take = to - from + 1; - - const [chats, totalCount] = await this.repo.findAndCount({ - order: { createDate: 'DESC' }, - skip, - take - }); - - logDatabase('Chats page retrieved successfully (including deleted)', undefined, Date.now() - startTime, { - from, - to, - returned: chats.length, - totalCount - }); - - return { chats, totalCount }; - } catch (error) { - logError('ChatRepository.findByPageIncludingDeleted error', error as Error); - throw new Error('Failed to retrieve chats page from database'); - } - } - - async findById(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOne({ - where: { - id, - state: Not(ChatState.SOFT_DELETE) - } - }); - logDatabase('Chat findById query completed', undefined, Date.now() - startTime, { - found: !!result, - chatId: id - }); - return result; - } catch (error) { - logError('ChatRepository.findById error', error as Error); - throw new Error('Failed to retrieve chat from database'); - } - } - - async findByIdIncludingDeleted(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ id }); - logDatabase('Chat findByIdIncludingDeleted query completed', undefined, Date.now() - startTime, { - found: !!result, - chatId: id - }); - return result; - } catch (error) { - logError('ChatRepository.findByIdIncludingDeleted error', error as Error); - throw new Error('Failed to retrieve chat from database'); - } - } - - async findByUserId(userId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .createQueryBuilder('chat') - .where(':userId = ANY(chat.users)', { userId }) - .andWhere('chat.state != :softDelete', { softDelete: ChatState.SOFT_DELETE }) - .getMany(); - - logDatabase('Chats retrieved by user id', `findByUserId(${userId})`, Date.now() - startTime, { - userId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatRepository.findByUserId error', error as Error); - throw new Error('Failed to find chats by user id'); - } - } - - async findByUserIdIncludingDeleted(userId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .createQueryBuilder('chat') - .where(':userId = ANY(chat.users)', { userId }) - .getMany(); - - logDatabase('Chats retrieved by user id (including deleted)', `findByUserIdIncludingDeleted(${userId})`, Date.now() - startTime, { - userId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatRepository.findByUserIdIncludingDeleted error', error as Error); - throw new Error('Failed to find all chats by user id'); - } - } - - async findByGameId(gameId: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ - gameId, - type: ChatType.GAME, - state: ChatState.ACTIVE - }); - logDatabase('Chat retrieved by game id', `findByGameId(${gameId})`, Date.now() - startTime, { - gameId, - found: !!result - }); - return result; - } catch (error) { - logError('ChatRepository.findByGameId error', error as Error); - throw new Error('Failed to find chat by game id'); - } - } - - async findActiveChatsForUser(userId: string) { - const startTime = Date.now(); - try { - const result = await this.repo - .createQueryBuilder('chat') - .where(':userId = ANY(chat.users)', { userId }) - .andWhere('chat.state = :state', { state: ChatState.ACTIVE }) - .orderBy('chat.lastActivity', 'DESC') - .getMany(); - - logDatabase('Active chats retrieved for user', `findActiveChatsForUser(${userId})`, Date.now() - startTime, { - userId, - count: result.length - }); - return result; - } catch (error) { - logError('ChatRepository.findActiveChatsForUser error', error as Error); - throw new Error('Failed to find active chats for user'); - } - } - - async findInactiveChats(inactivityMinutes: number) { - const startTime = Date.now(); - try { - const cutoffDate = new Date(Date.now() - inactivityMinutes * 60 * 1000); - - const result = await this.repo - .createQueryBuilder('chat') - .where('chat.state = :state', { state: ChatState.ACTIVE }) - .andWhere('(chat.lastActivity < :cutoffDate OR chat.lastActivity IS NULL)', { cutoffDate }) - .getMany(); - - logDatabase('Inactive chats retrieved', `findInactiveChats(${inactivityMinutes}min)`, Date.now() - startTime, { - inactivityMinutes, - count: result.length, - cutoffDate - }); - return result; - } catch (error) { - logError('ChatRepository.findInactiveChats error', error as Error); - throw new Error('Failed to find inactive chats'); - } - } - - async update(id: string, update: Partial) { - const startTime = Date.now(); - try { - await this.repo.update(id, update); - const result = await this.findById(id); - logDatabase('Chat updated successfully', `update(${id})`, Date.now() - startTime, { - chatId: id, - updatedFields: Object.keys(update), - success: !!result - }); - return result; - } catch (error) { - logError('ChatRepository.update error', error as Error); - throw new Error('Failed to update chat in database'); - } - } - - async delete(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.delete(id); - logDatabase('Chat deleted', `delete(${id})`, Date.now() - startTime, { - chatId: id, - affected: result.affected - }); - return result; - } catch (error) { - logError('ChatRepository.delete error', error as Error); - throw new Error('Failed to delete chat'); - } - } - - async softDelete(id: string) { - const startTime = Date.now(); - try { - await this.repo.update(id, { state: ChatState.SOFT_DELETE }); - const result = await this.findById(id); - logDatabase('Chat soft deleted', `softDelete(${id})`, Date.now() - startTime, { - chatId: id, - success: !!result - }); - return result; - } catch (error) { - logError('ChatRepository.softDelete error', error as Error); - throw new Error('Failed to soft delete chat'); - } - } - - async archiveChat(chat: ChatAggregate) { - const startTime = Date.now(); - try { - const archive = new ChatArchiveAggregate(); - archive.chatId = chat.id; - archive.archivedMessages = chat.messages; - archive.archivedAt = new Date(); - archive.chatType = chat.type; - archive.chatName = chat.name; - archive.gameId = chat.gameId; - archive.participants = chat.users; - - const archivedResult = await this.archiveRepo.save(archive); - - await this.repo.update(chat.id, { - state: ChatState.ARCHIVE, - messages: [], - archiveDate: new Date() - }); - - logDatabase('Chat archived successfully', `archiveChat(${chat.id})`, Date.now() - startTime, { - chatId: chat.id, - messageCount: chat.messages.length, - archiveId: archivedResult.id - }); - - return archivedResult; - } catch (error) { - logError('ChatRepository.archiveChat error', error as Error); - throw new Error('Failed to archive chat'); - } - } - - async getArchivedChat(chatId: string) { - const startTime = Date.now(); - try { - const result = await this.archiveRepo.findOneBy({ chatId }); - logDatabase('Archived chat retrieved', `getArchivedChat(${chatId})`, Date.now() - startTime, { - chatId, - found: !!result - }); - return result; - } catch (error) { - logError('ChatRepository.getArchivedChat error', error as Error); - throw new Error('Failed to retrieve archived chat'); - } - } - - async restoreFromArchive(chatId: string) { - const startTime = Date.now(); - try { - const archive = await this.archiveRepo.findOneBy({ chatId }); - if (!archive) { - return null; - } - - // Game chats cannot be restored, only viewed - if (archive.chatType === ChatType.GAME) { - logDatabase('Game chat restore attempt blocked', `restoreFromArchive(${chatId})`, Date.now() - startTime, { - chatId, - chatType: archive.chatType, - blocked: true - }); - return null; - } - - // Restore messages to the chat - await this.repo.update(chatId, { - state: ChatState.ACTIVE, - messages: archive.archivedMessages, - lastActivity: new Date(), - archiveDate: null - }); - - const result = await this.findById(chatId); - logDatabase('Chat restored from archive', `restoreFromArchive(${chatId})`, Date.now() - startTime, { - chatId, - messageCount: archive.archivedMessages.length, - success: !!result - }); - - return result; - } catch (error) { - logError('ChatRepository.restoreFromArchive error', error as Error); - throw new Error('Failed to restore chat from archive'); - } - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/ContactRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/ContactRepository.ts deleted file mode 100644 index dab30922..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/ContactRepository.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Repository, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { ContactAggregate, ContactState } from '../../Domain/Contact/ContactAggregate'; -import { IContactRepository } from '../../Domain/IRepository/IContactRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class ContactRepository implements IContactRepository { - private repo: Repository; - - constructor() { - this.repo = AppDataSource.getRepository(ContactAggregate); - } - - async create(contact: Partial) { - return this.repo.save(contact); - } - - async findById(id: string) { - return this.repo - .createQueryBuilder('contact') - .where('contact.id = :id', { id }) - .andWhere('contact.state != :softDelete', { softDelete: ContactState.SOFT_DELETE }) - .getOne(); - } - - async findByPage(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { - state: Not(ContactState.SOFT_DELETE) - } - }); - - // Get paginated results - const contacts = await this.repo - .createQueryBuilder('contact') - .where('contact.state != :softDelete', { softDelete: ContactState.SOFT_DELETE }) - .orderBy('contact.createDate', 'DESC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Contact page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${contacts.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { contacts, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Contact page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('ContactRepository.findByPage error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get contacts page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count(); - - // Get paginated results - const contacts = await this.repo - .createQueryBuilder('contact') - .orderBy('contact.createDate', 'DESC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Contact page query completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${contacts.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { contacts, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Contact page query failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('ContactRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get contacts page from database'); - } - } - - async update(id: string, update: Partial) { - await this.repo.update(id, update); - return this.findById(id); - } - - async delete(id: string) { - return this.repo.delete(id); - } - - async softDelete(id: string) { - await this.repo.update(id, { state: ContactState.SOFT_DELETE }); - return this.findById(id); - } - - async findByIdIncludingDeleted(id: string) { - return this.repo.findOneBy({ id }); // Returns contact regardless of state - } - - async searchIncludingDeleted(searchTerm: string) { - return this.repo - .createQueryBuilder('contact') - .where('contact.name ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .orWhere('contact.email ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .orWhere('contact.txt ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .getMany(); - } - - async search(searchTerm: string) { - return this.repo - .createQueryBuilder('contact') - .where('contact.name ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .orWhere('contact.email ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .orWhere('contact.txt ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` }) - .andWhere('contact.state != :softDelete', { softDelete: ContactState.SOFT_DELETE }) - .getMany(); - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/DeckRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/DeckRepository.ts deleted file mode 100644 index 3c50b183..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/DeckRepository.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { Repository, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { DeckAggregate, State, CType } from '../../Domain/Deck/DeckAggregate'; -import { IDeckRepository } from '../../Domain/IRepository/IDeckRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; -import { AdminBypassService } from '../../Application/Services/AdminBypassService'; - -export class DeckRepository implements IDeckRepository { - private repo: Repository; - constructor() { - this.repo = AppDataSource.getRepository(DeckAggregate); - } - - async create(deck: Partial) { - return this.repo.save(deck); - } - - async findByPage(from: number, to: number): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { state: Not(State.SOFT_DELETE) } - }); - - // Get paginated results - const decks = await this.repo.find({ - where: { state: Not(State.SOFT_DELETE) }, - order: { updateDate: 'DESC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Deck page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Deck page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('DeckRepository.findByPage error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get decks page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count(); - - // Get paginated results - const decks = await this.repo.find({ - order: { updateDate: 'DESC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Deck page query completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Deck page query failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('DeckRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get decks page from database'); - } - } - - async findById(id: string) { - return this.repo.findOne({ - where: { - id, - state: Not(State.SOFT_DELETE) - } - }); - } - - async findByIdIncludingDeleted(id: string) { - return this.repo.findOneBy({ id }); - } - - async update(id: string, update: Partial) { - await this.repo.update(id, update); - return this.findByIdIncludingDeleted(id); - } - - async delete(id: string) { - return this.repo.delete(id); - } - - async softDelete(id: string) { - await this.repo.update(id, { state: State.SOFT_DELETE }); - return this.findById(id); - } - - async search(query: string, limit: number = 20, offset: number = 0): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('deck') - .where('deck.state != :softDelete', { softDelete: State.SOFT_DELETE }) - .andWhere('LOWER(deck.name) LIKE :pattern', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const decks = await queryBuilder - .orderBy('deck.name', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Deck search completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Deck search failed', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`); - logError('DeckRepository.search error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search decks in database'); - } - } - - async searchIncludingDeleted(query: string, limit: number = 20, offset: number = 0): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('deck') - .where('LOWER(deck.name) LIKE :pattern', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const decks = await queryBuilder - .orderBy('deck.name', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Deck search completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Deck search failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`); - logError('DeckRepository.searchIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search all decks in database'); - } - } - - /** - * Count active (non-soft-deleted) decks for a specific user - * @param userId - User ID to count decks for - * @returns Number of active decks - */ - async countActiveByUserId(userId: string): Promise { - const startTime = performance.now(); - try { - const count = await this.repo.count({ - where: { - userid: userId, - state: Not(State.SOFT_DELETE) - } - }); - - const endTime = performance.now(); - logDatabase('User active deck count completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, count: ${count}`); - - return count; - } catch (error) { - const endTime = performance.now(); - logDatabase('User active deck count failed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}`); - logError('DeckRepository.countActiveByUserId error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to count active decks for user'); - } - } - - /** - * Count organizational decks for a specific user - * @param userId - User ID to count organizational decks for - * @returns Number of organizational decks - */ - async countOrganizationalByUserId(userId: string): Promise { - const startTime = performance.now(); - try { - const count = await this.repo.count({ - where: { - userid: userId, - ctype: CType.ORGANIZATION, - state: Not(State.SOFT_DELETE) - } - }); - - const endTime = performance.now(); - logDatabase('User organizational deck count completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, count: ${count}`); - - return count; - } catch (error) { - const endTime = performance.now(); - logDatabase('User organizational deck count failed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}`); - logError('DeckRepository.countOrganizationalByUserId error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to count organizational decks for user'); - } - } - - /** - * Find decks with filtering based on user permissions and mandatory pagination - * @param userId - User ID for filtering - * @param userOrgId - User's organization ID (if any) - * @param isAdmin - Whether user is admin (bypasses filtering) - * @param from - Start index for pagination (default: 0) - * @param to - End index for pagination (default: 49) - * @returns Paginated filtered list of decks with total count - */ - async findFilteredDecks(userId: string, userOrgId?: string | null, isAdmin?: boolean, from: number = 0, to: number = 49): Promise<{ decks: DeckAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - // Validate pagination parameters - if (from < 0 || to < from) { - throw new Error('Invalid pagination parameters'); - } - - const limit = to - from + 1; - if (limit > 100) { - throw new Error('Page size too large. Maximum 100 records per request'); - } - - const skip = from; - const take = limit; - - // Admin gets ALL decks with pagination - if (isAdmin) { - AdminBypassService.logAdminBypass( - 'FIND_FILTERED_DECKS_BYPASS', - userId, - 'all-decks-filtered', - { - bypassType: 'admin-all-decks-filtered', - userOrgId, - from, - to, - operation: 'read' - } - ); - - const [decks, totalCount] = await this.repo.findAndCount({ - where: { state: Not(State.SOFT_DELETE) }, - relations: ['organization', 'user'], - order: { creationdate: 'DESC' }, - skip, - take - }); - - const endTime = performance.now(); - logDatabase('Admin filtered deck query completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, found: ${decks.length}, totalCount: ${totalCount}, isAdmin: true`); - - return { decks, totalCount }; - } - - // Regular user complex filtering - const queryBuilder = this.repo.createQueryBuilder('deck') - .leftJoinAndSelect('deck.organization', 'org') - .leftJoinAndSelect('deck.user', 'user') - .where('deck.state != :deletedState', { deletedState: State.SOFT_DELETE }); - - queryBuilder.andWhere('(' + - // User's private decks - '(deck.userid = :userId AND deck.ctype = :privateType) OR ' + - // All public decks - '(deck.ctype = :publicType)' + - // Organization decks from same org (if user has org) - (userOrgId ? ' OR (deck.ctype = :orgType AND org.id = :orgId)' : '') + - ')', { - userId, - privateType: CType.PRIVATE, - publicType: CType.PUBLIC, - ...(userOrgId && { orgType: CType.ORGANIZATION, orgId: userOrgId }) - }); - - queryBuilder - .orderBy('deck.creationdate', 'DESC') - .skip(skip) - .take(take); - - const [decks, totalCount] = await queryBuilder.getManyAndCount(); - - const endTime = performance.now(); - logDatabase('User filtered deck query completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, userOrgId: ${userOrgId}, found: ${decks.length}, totalCount: ${totalCount}, isAdmin: false`); - - return { decks, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Filtered deck query failed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, isAdmin: ${isAdmin}`); - logError('DeckRepository.findFilteredDecks error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find filtered decks'); - } - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/GameRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/GameRepository.ts deleted file mode 100644 index d16dfbc0..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/GameRepository.ts +++ /dev/null @@ -1,416 +0,0 @@ -import { Repository, Not, In } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { GameAggregate, GameState } from '../../Domain/Game/GameAggregate'; -import { IGameRepository } from '../../Domain/IRepository/IGameRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class GameRepository implements IGameRepository { - private repo: Repository; - constructor() { - this.repo = AppDataSource.getRepository(GameAggregate); - } - - async create(game: Partial): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.save(game); - const endTime = performance.now(); - logDatabase('Game created', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${result.id}, gameCode: ${result.gamecode}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game creation failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - logError('GameRepository.create error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to create game in database'); - } - } - - async findByPage(from: number, to: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { state: Not(GameState.CANCELLED) } - }); - - // Get paginated results - const games = await this.repo.find({ - where: { state: Not(GameState.CANCELLED) }, - order: { updateDate: 'DESC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Game page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('GameRepository.findByPage error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get games page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination (including deleted) - const totalCount = await this.repo.count(); - - // Get paginated results (including deleted) - const games = await this.repo.find({ - order: { updateDate: 'DESC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Game page query (including deleted) completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game page query (including deleted) failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('GameRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get games page (including deleted) from database'); - } - } - - async findById(id: string): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.findOne({ - where: { id, state: Not(GameState.CANCELLED) } - }); - const endTime = performance.now(); - logDatabase('Game findById completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, found: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game findById failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.findById error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find game by id in database'); - } - } - - async findByIdIncludingDeleted(id: string): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.findOne({ - where: { id } - }); - const endTime = performance.now(); - logDatabase('Game findByIdIncludingDeleted completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, found: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game findByIdIncludingDeleted failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.findByIdIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find game by id (including deleted) in database'); - } - } - - async findByGameCode(gamecode: string): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.findOne({ - where: { gamecode, state: Not(GameState.CANCELLED) } - }); - const endTime = performance.now(); - logDatabase('Game findByGameCode completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameCode: ${gamecode}, found: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game findByGameCode failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameCode: ${gamecode}`); - logError('GameRepository.findByGameCode error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find game by game code in database'); - } - } - - async search(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const queryBuilder = this.repo.createQueryBuilder('game') - .where('game.state != :cancelledState', { cancelledState: GameState.CANCELLED }) - .andWhere('(game.gamecode ILIKE :query)', { query: `%${query}%` }); - - // Get total count - const totalCount = await queryBuilder.getCount(); - - // Apply pagination if provided - if (limit !== undefined) { - queryBuilder.take(limit); - } - if (offset !== undefined) { - queryBuilder.skip(offset); - } - - const games = await queryBuilder.orderBy('game.updateDate', 'DESC').getMany(); - - const endTime = performance.now(); - logDatabase('Game search completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`); - - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game search failed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}`); - logError('GameRepository.search error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search games in database'); - } - } - - async searchIncludingDeleted(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const queryBuilder = this.repo.createQueryBuilder('game') - .where('(game.gamecode ILIKE :query)', { query: `%${query}%` }); - - // Get total count - const totalCount = await queryBuilder.getCount(); - - // Apply pagination if provided - if (limit !== undefined) { - queryBuilder.take(limit); - } - if (offset !== undefined) { - queryBuilder.skip(offset); - } - - const games = await queryBuilder.orderBy('game.updateDate', 'DESC').getMany(); - - const endTime = performance.now(); - logDatabase('Game search (including deleted) completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`); - - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game search (including deleted) failed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}`); - logError('GameRepository.searchIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search games (including deleted) in database'); - } - } - - async update(id: string, update: Partial): Promise { - const startTime = performance.now(); - try { - await this.repo.update(id, update); - const result = await this.findById(id); - const endTime = performance.now(); - logDatabase('Game update completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, updated: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game update failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.update error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to update game in database'); - } - } - - async delete(id: string): Promise { - const startTime = performance.now(); - try { - const result = await this.repo.delete(id); - const endTime = performance.now(); - logDatabase('Game delete completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, affected: ${result.affected}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game delete failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.delete error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to delete game from database'); - } - } - - async softDelete(id: string): Promise { - const startTime = performance.now(); - try { - await this.repo.update(id, { state: GameState.CANCELLED }); - const result = await this.findByIdIncludingDeleted(id); - const endTime = performance.now(); - logDatabase('Game soft delete completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, updated: ${!!result}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game soft delete failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`); - logError('GameRepository.softDelete error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to soft delete game in database'); - } - } - - // Game-specific methods - async findActiveGames(): Promise { - const startTime = performance.now(); - try { - const games = await this.repo.find({ - where: { state: GameState.ACTIVE }, - order: { updateDate: 'DESC' } - }); - const endTime = performance.now(); - logDatabase('Active games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}`); - return games; - } catch (error) { - const endTime = performance.now(); - logDatabase('Active games query failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - logError('GameRepository.findActiveGames error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find active games in database'); - } - } - - async findGamesByPlayer(playerId: string): Promise { - const startTime = performance.now(); - try { - const queryBuilder = this.repo.createQueryBuilder('game') - .where('game.state != :cancelledState', { cancelledState: GameState.CANCELLED }) - .andWhere('JSON_CONTAINS(game.players, :playerId)', { playerId: `"${playerId}"` }) - .orderBy('game.updateDate', 'DESC'); - - const games = await queryBuilder.getMany(); - const endTime = performance.now(); - logDatabase('Games by player query completed', `executionTime: ${Math.round(endTime - startTime)}ms, playerId: ${playerId}, found: ${games.length}`); - return games; - } catch (error) { - const endTime = performance.now(); - logDatabase('Games by player query failed', `executionTime: ${Math.round(endTime - startTime)}ms, playerId: ${playerId}`); - logError('GameRepository.findGamesByPlayer error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find games by player in database'); - } - } - - async findWaitingGames(): Promise { - const startTime = performance.now(); - try { - const games = await this.repo.find({ - where: { state: GameState.WAITING }, - order: { createdate: 'ASC' } - }); - const endTime = performance.now(); - logDatabase('Waiting games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}`); - return games; - } catch (error) { - const endTime = performance.now(); - logDatabase('Waiting games query failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - logError('GameRepository.findWaitingGames error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find waiting games in database'); - } - } - - async findFinishedGames(from?: number, to?: number): Promise<{ games: GameAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const queryBuilder = this.repo.createQueryBuilder('game') - .where('game.state = :finishedState', { finishedState: GameState.FINISHED }) - .orderBy('game.enddate', 'DESC'); - - // Get total count - const totalCount = await queryBuilder.getCount(); - - // Apply pagination if provided - if (from !== undefined && to !== undefined) { - const limit = to - from + 1; - const offset = from; - queryBuilder.take(limit).skip(offset); - } - - const games = await queryBuilder.getMany(); - const endTime = performance.now(); - logDatabase('Finished games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}, total: ${totalCount}`); - return { games, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Finished games query failed', `executionTime: ${Math.round(endTime - startTime)}ms`); - logError('GameRepository.findFinishedGames error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to find finished games in database'); - } - } - - async addPlayerToGame(gameId: string, playerId: string): Promise { - const startTime = performance.now(); - try { - const game = await this.findById(gameId); - if (!game) { - return null; - } - - // Check if player is already in the game - if (game.players.includes(playerId)) { - return game; - } - - // Check if game is full - if (game.players.length >= game.maxplayers) { - throw new Error('Game is full'); - } - - const updatedPlayers = [...game.players, playerId]; - const result = await this.update(gameId, { players: updatedPlayers }); - - const endTime = performance.now(); - logDatabase('Player added to game', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Add player to game failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`); - logError('GameRepository.addPlayerToGame error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to add player to game in database'); - } - } - - async removePlayerFromGame(gameId: string, playerId: string): Promise { - const startTime = performance.now(); - try { - const game = await this.findById(gameId); - if (!game) { - return null; - } - - const updatedPlayers = game.players.filter(id => id !== playerId); - const result = await this.update(gameId, { players: updatedPlayers }); - - const endTime = performance.now(); - logDatabase('Player removed from game', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Remove player from game failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`); - logError('GameRepository.removePlayerFromGame error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to remove player from game in database'); - } - } - - async updateGameState(gameId: string, state: GameState, winner?: string): Promise { - const startTime = performance.now(); - try { - const updateData: Partial = { state }; - - if (state === GameState.ACTIVE) { - updateData.startdate = new Date(); - } - - if (state === GameState.FINISHED) { - updateData.enddate = new Date(); - if (winner) { - updateData.winnerId = winner; - } - } - - const result = await this.update(gameId, updateData); - - const endTime = performance.now(); - logDatabase('Game state updated', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, state: ${updateData.state}, winner: ${winner}`); - return result; - } catch (error) { - const endTime = performance.now(); - logDatabase('Game state update failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}`); - logError('GameRepository.updateGameState error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to update game state in database'); - } - } -} \ No newline at end of file diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/GameSnapshotRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/GameSnapshotRepository.ts deleted file mode 100644 index 15107377..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/GameSnapshotRepository.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Repository, LessThan } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { IGameSnapshotRepository } from '../../Domain/IRepository/IGameSnapshotRepository'; -import { GameSnapshotAggregate, SnapshotTrigger } from '../../Domain/Game/GameSnapshotAggregate'; - -export class GameSnapshotRepository implements IGameSnapshotRepository { - private repository: Repository; - - constructor() { - this.repository = AppDataSource.getRepository(GameSnapshotAggregate); - } - - async save(snapshot: GameSnapshotAggregate): Promise { - return await this.repository.save(snapshot); - } - - async findLatestByGameId(gameId: string): Promise { - return await this.repository.findOne({ - where: { gameid: gameId }, - order: { createdat: 'DESC' } - }); - } - - async findByGameId(gameId: string): Promise { - return await this.repository.find({ - where: { gameid: gameId }, - order: { turnNumber: 'ASC', createdat: 'ASC' } - }); - } - - async findByGameAndTrigger(gameId: string, trigger: SnapshotTrigger): Promise { - return await this.repository.find({ - where: { - gameid: gameId, - trigger: trigger - }, - order: { createdat: 'DESC' } - }); - } - - async findByGameAndTurn(gameId: string, turnNumber: number): Promise { - return await this.repository.findOne({ - where: { - gameid: gameId, - turnNumber: turnNumber - }, - order: { createdat: 'DESC' } - }); - } - - async deleteOldSnapshots(gameId: string, keepCount: number): Promise { - const snapshots = await this.repository.find({ - where: { gameid: gameId }, - order: { createdat: 'DESC' }, - select: ['id', 'createdat'] - }); - - if (snapshots.length > keepCount) { - const idsToDelete = snapshots - .slice(keepCount) - .map(s => s.id); - - if (idsToDelete.length > 0) { - await this.repository.delete(idsToDelete); - } - } - } - - async deleteByGameId(gameId: string): Promise { - await this.repository.delete({ gameid: gameId }); - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/OrganizationRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/OrganizationRepository.ts deleted file mode 100644 index f8647e60..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/OrganizationRepository.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Repository, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { OrganizationAggregate, OrganizationState } from '../../Domain/Organization/OrganizationAggregate'; -import { IOrganizationRepository } from '../../Domain/IRepository/IOrganizationRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class OrganizationRepository implements IOrganizationRepository { - private repo: Repository; - constructor() { - this.repo = AppDataSource.getRepository(OrganizationAggregate); - } - - async create(org: Partial) { - return this.repo.save(org); - } - - async findByPage(from: number, to: number): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { state: Not(OrganizationState.SOFT_DELETE) } - }); - - // Get paginated results - const organizations = await this.repo.find({ - where: { state: Not(OrganizationState.SOFT_DELETE) }, - order: { name: 'ASC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Organization page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { organizations, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Organization page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('OrganizationRepository.findByPage error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get organizations page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count(); - - // Get paginated results - const organizations = await this.repo.find({ - order: { name: 'ASC' }, - take: limit, - skip: offset - }); - - const endTime = performance.now(); - logDatabase('Organization page query completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, from: ${from}, to: ${to}`); - - return { organizations, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Organization page query failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`); - logError('OrganizationRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to get organizations page from database'); - } - } - - async findById(id: string) { - return this.repo.findOne({ - where: { - id, - state: Not(OrganizationState.SOFT_DELETE) - } - }); - } - - async findByIdIncludingDeleted(id: string) { - return this.repo.findOneBy({ id }); - } - - async update(id: string, update: Partial) { - await this.repo.update(id, update); - return this.findById(id); - } - - async delete(id: string) { - return this.repo.delete(id); - } - - async softDelete(id: string) { - await this.repo.update(id, { state: OrganizationState.SOFT_DELETE }); - return this.findById(id); - } - - async search(query: string, limit: number = 20, offset: number = 0): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('org') - .where('org.state != :softDelete', { softDelete: OrganizationState.SOFT_DELETE }) - .andWhere('(LOWER(org.name) LIKE :pattern OR LOWER(org.contactfname) LIKE :pattern OR LOWER(org.contactlname) LIKE :pattern OR LOWER(org.contactemail) LIKE :pattern OR LOWER(CONCAT(org.contactfname, \' \', org.contactlname)) LIKE :pattern)', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const organizations = await queryBuilder - .orderBy('org.name', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Organization search completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`); - - return { organizations, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Organization search failed', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`); - logError('OrganizationRepository.search error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search organizations in database'); - } - } - - async searchIncludingDeleted(query: string, limit: number = 20, offset: number = 0): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> { - const startTime = performance.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('org') - .where('LOWER(org.name) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(org.contactfname) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(org.contactlname) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(org.contactemail) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(CONCAT(org.contactfname, \' \', org.contactlname)) LIKE :pattern', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const organizations = await queryBuilder - .orderBy('org.name', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - const endTime = performance.now(); - logDatabase('Organization search completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`); - - return { organizations, totalCount }; - } catch (error) { - const endTime = performance.now(); - logDatabase('Organization search failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`); - logError('OrganizationRepository.searchIncludingDeleted error', error instanceof Error ? error : new Error(String(error))); - throw new Error('Failed to search all organizations in database'); - } - } - -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/TurnHistoryRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/TurnHistoryRepository.ts deleted file mode 100644 index 5b801210..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/TurnHistoryRepository.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Repository } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { ITurnHistoryRepository } from '../../Domain/IRepository/ITurnHistoryRepository'; -import { TurnHistoryAggregate } from '../../Domain/Game/TurnHistoryAggregate'; - -export class TurnHistoryRepository implements ITurnHistoryRepository { - private repository: Repository; - - constructor() { - this.repository = AppDataSource.getRepository(TurnHistoryAggregate); - } - - async save(turnHistory: TurnHistoryAggregate): Promise { - return await this.repository.save(turnHistory); - } - - async findByGameId(gameId: string): Promise { - return await this.repository.find({ - where: { gameid: gameId }, - order: { turnNumber: 'ASC', createdat: 'ASC' } - }); - } - - async findByGameAndPlayer(gameId: string, playerId: string): Promise { - return await this.repository.find({ - where: { - gameid: gameId, - playerid: playerId - }, - order: { turnNumber: 'ASC', createdat: 'ASC' } - }); - } - - async findLastNTurns(gameId: string, limit: number): Promise { - return await this.repository.find({ - where: { gameid: gameId }, - order: { turnNumber: 'DESC', createdat: 'DESC' }, - take: limit - }); - } - - async countTurnsByGame(gameId: string): Promise { - return await this.repository.count({ - where: { gameid: gameId } - }); - } - - async deleteByGameId(gameId: string): Promise { - await this.repository.delete({ gameid: gameId }); - } -} diff --git a/SerpentRace_Backend/src/Infrastructure/Repository/UserRepository.ts b/SerpentRace_Backend/src/Infrastructure/Repository/UserRepository.ts deleted file mode 100644 index 40d772ae..00000000 --- a/SerpentRace_Backend/src/Infrastructure/Repository/UserRepository.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { Repository, Not } from 'typeorm'; -import { AppDataSource } from '../ormconfig'; -import { UserAggregate, UserState } from '../../Domain/User/UserAggregate'; -import { IUserRepository } from '../../Domain/IRepository/IUserRepository'; -import { logDatabase, logError } from '../../Application/Services/Logger'; - -export class UserRepository implements IUserRepository { - private repo: Repository; - constructor() { - this.repo = AppDataSource.getRepository(UserAggregate); - } - - async create(user: Partial) { - const startTime = Date.now(); - try { - const result = await this.repo.save(user); - logDatabase('User created successfully', undefined, Date.now() - startTime, { - userId: result.id, - username: user.username, - email: user.email - }); - return result; - } catch (error) { - logError('UserRepository.create error', error as Error); - - // Handle unique constraint violations - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique'))) { - throw new Error('User with this username or email already exists'); - } - - throw new Error('Failed to create user in database'); - } - } - - async findByPage(from: number, to: number): Promise<{ users: UserAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count({ - where: { state: Not(UserState.SOFT_DELETE) } - }); - - // Get paginated results - const users = await this.repo.find({ - where: { state: Not(UserState.SOFT_DELETE) }, - order: { regdate: 'DESC' }, - take: limit, - skip: offset - }); - - logDatabase('User page query completed', `from: ${from}, to: ${to}`, Date.now() - startTime, { - found: users.length, - total: totalCount - }); - - return { users, totalCount }; - } catch (error) { - logError('UserRepository.findByPage error', error as Error); - throw new Error('Failed to get users page from database'); - } - } - - async findByPageIncludingDeleted(from: number, to: number): Promise<{ users: UserAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const limit = to - from + 1; - const offset = from; - - // Get total count for pagination - const totalCount = await this.repo.count(); - - // Get paginated results - const users = await this.repo.find({ - order: { regdate: 'DESC' }, - take: limit, - skip: offset - }); - - logDatabase('User page query completed (including deleted)', `from: ${from}, to: ${to}`, Date.now() - startTime, { - found: users.length, - total: totalCount - }); - - return { users, totalCount }; - } catch (error) { - logError('UserRepository.findByPageIncludingDeleted error', error as Error); - throw new Error('Failed to get users page from database'); - } - } - - async findById(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOne({ - where: { - id, - state: Not(UserState.SOFT_DELETE) - } - }); - logDatabase('User findById query completed', `findOneBy({ id: ${id} })`, Date.now() - startTime, { - found: !!result, - userId: id - }); - return result; - } catch (error) { - logError('UserRepository.findById error', error as Error); - - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - return null; - } - - throw new Error('Failed to retrieve user from database'); - } - } - - async findByIdIncludingDeleted(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ id }); - logDatabase('User findByIdIncludingDeleted query completed', `findOneBy({ id: ${id} })`, Date.now() - startTime, { - found: !!result, - userId: id - }); - return result; - } catch (error) { - logError('UserRepository.findByIdIncludingDeleted error', error as Error); - - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - return null; - } - - throw new Error('Failed to retrieve user from database'); - } - } - - async findByUsername(username: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ username }); - logDatabase('User findByUsername query completed', `findOneBy({ username: ${username} })`, Date.now() - startTime, { - found: !!result, - username - }); - return result; - } catch (error) { - logError('UserRepository.findByUsername error', error as Error); - throw new Error('Failed to retrieve user by username from database'); - } - } - - async findByEmail(email: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ email }); - logDatabase('User findByEmail query completed', `findOneBy({ email: ${email} })`, Date.now() - startTime, { - found: !!result, - email - }); - return result; - } catch (error) { - logError('UserRepository.findByEmail error', error as Error); - throw new Error('Failed to retrieve user by email from database'); - } - } - - async findByToken(token: string) { - const startTime = Date.now(); - try { - const result = await this.repo.findOneBy({ token: token }); - logDatabase('User findByToken query completed', `findOneBy({ token })`, Date.now() - startTime, { - found: !!result, - tokenPrefix: token.substring(0, 8) + '...' - }); - return result; - } catch (error) { - logError('UserRepository.findByToken error', error as Error); - throw new Error('Failed to retrieve user by token from database'); - } - } - - async update(id: string, update: Partial) { - const startTime = Date.now(); - try { - await this.repo.update(id, update); - const result = await this.findById(id); - logDatabase('User updated successfully', `update(${id})`, Date.now() - startTime, { - userId: id, - updatedFields: Object.keys(update), - success: !!result - }); - return result; - } catch (error) { - logError('UserRepository.update error', error as Error); - - // Handle unique constraint violations - if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique'))) { - throw new Error('Username or email already exists'); - } - - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - - throw new Error('Failed to update user in database'); - } - } - - async delete(id: string) { - const startTime = Date.now(); - try { - const result = await this.repo.delete(id); - logDatabase('User deleted successfully', `delete(${id})`, Date.now() - startTime, { - userId: id, - affected: result.affected - }); - return result; - } catch (error) { - logError('UserRepository.delete error', error as Error); - - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - - throw new Error('Failed to delete user from database'); - } - } - - async softDelete(id: string) { - const startTime = Date.now(); - try { - await this.repo.update(id, { state: UserState.SOFT_DELETE }); - const result = await this.findById(id); - logDatabase('User soft deleted successfully', `update(${id}, { state: SOFT_DELETE })`, Date.now() - startTime, { - userId: id, - success: !!result - }); - return result; - } catch (error) { - logError('UserRepository.softDelete error', error as Error); - - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - - throw new Error('Failed to soft delete user in database'); - } - } - - async deactivate(id: string) { - const startTime = Date.now(); - try { - await this.repo.update(id, { state: UserState.DEACTIVATED }); - const result = await this.findById(id); - logDatabase('User deactivated successfully', `update(${id}, { state: DEACTIVATED })`, Date.now() - startTime, { - userId: id, - success: !!result - }); - return result; - } catch (error) { - logError('UserRepository.deactivate error', error as Error); - - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - - throw new Error('Failed to deactivate user in database'); - } - } - - async search(query: string, limit: number = 20, offset: number = 0): Promise<{ users: UserAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('user') - .where('user.state != :softDelete', { softDelete: UserState.SOFT_DELETE }) - .andWhere('(LOWER(user.username) LIKE :pattern OR LOWER(user.email) LIKE :pattern OR LOWER(user.fname) LIKE :pattern OR LOWER(user.lname) LIKE :pattern OR LOWER(CONCAT(user.fname, \' \', user.lname)) LIKE :pattern)', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const users = await queryBuilder - .orderBy('user.username', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - logDatabase('User search completed', - `search query: ${query.substring(0, 50)}...`, - Date.now() - startTime, { - query, - limit, - offset, - totalCount, - returnedCount: users.length - }); - - return { users, totalCount }; - } catch (error) { - logError('UserRepository.search error', error as Error); - throw new Error('Failed to search users in database'); - } - } - - async searchIncludingDeleted(query: string, limit: number = 20, offset: number = 0): Promise<{ users: UserAggregate[], totalCount: number }> { - const startTime = Date.now(); - try { - const searchPattern = `%${query.toLowerCase()}%`; - - const queryBuilder = this.repo.createQueryBuilder('user') - .where('LOWER(user.username) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(user.email) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(user.fname) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(user.lname) LIKE :pattern', { pattern: searchPattern }) - .orWhere('LOWER(CONCAT(user.fname, \' \', user.lname)) LIKE :pattern', { pattern: searchPattern }); - - const totalCount = await queryBuilder.getCount(); - - const users = await queryBuilder - .orderBy('user.username', 'ASC') - .limit(limit) - .offset(offset) - .getMany(); - - logDatabase('User search completed (including deleted)', - `search query: ${query.substring(0, 50)}...`, - Date.now() - startTime, { - query, - limit, - offset, - totalCount, - returnedCount: users.length - }); - - return { users, totalCount }; - } catch (error) { - logError('UserRepository.searchIncludingDeleted error', error as Error); - throw new Error('Failed to search all users in database'); - } - } - - async activate(id: string) { - const startTime = Date.now(); - try { - await this.repo.update(id, { state: UserState.VERIFIED_REGULAR }); - const result = await this.findById(id); - logDatabase('User activated successfully', `update(${id}, { state: VERIFIED_REGULAR })`, Date.now() - startTime, { - userId: id, - success: !!result - }); - return result; - } - catch (error) { - logError('UserRepository.activate error', error as Error); - // Handle invalid UUID format - if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) { - throw new Error('Invalid user ID format'); - } - throw new Error('Failed to activate user in database'); - } - } - -} diff --git a/SerpentRace_Backend/src/Infrastructure/ormconfig.ts b/SerpentRace_Backend/src/Infrastructure/ormconfig.ts deleted file mode 100644 index 939b2117..00000000 --- a/SerpentRace_Backend/src/Infrastructure/ormconfig.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DataSource } from 'typeorm'; -import { join } from 'path'; - -export const AppDataSource = new DataSource({ - type: 'postgres', - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432'), - username: process.env.DB_USERNAME || 'postgres', - password: process.env.DB_PASSWORD || 'postgres', - database: process.env.DB_NAME || 'serpentrace', - synchronize: false, // Set to false when using migrations - logging: process.env.NODE_ENV === 'development', - entities: [join(__dirname, '../Domain/**/*Aggregate.ts')], - migrations: [join(__dirname, './Migrations/*.ts')], - migrationsTableName: 'migrations', - migrationsRun: false // Let migrations run manually -}); \ No newline at end of file diff --git a/SerpentRace_Backend/src/Templates/contact-response-de.html b/SerpentRace_Backend/src/Templates/contact-response-de.html deleted file mode 100644 index bf1a0e0a..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response-de.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - {{companyName}} - Antwort auf Ihre {{contactTypeString}} - - - - - - diff --git a/SerpentRace_Backend/src/Templates/contact-response-de.txt b/SerpentRace_Backend/src/Templates/contact-response-de.txt deleted file mode 100644 index b02f1fcb..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response-de.txt +++ /dev/null @@ -1,21 +0,0 @@ -{{companyName}} - Antwort auf Ihre {{contactTypeString}} - -Hallo {{contactName}}, - -Vielen Dank, dass Sie uns kontaktiert haben! Wir haben Ihre Nachricht geprüft und unser Team hat die folgende Antwort vorbereitet. - -=== IHRE URSPRÜNGLICHE NACHRICHT ({{contactTypeString}}) === -{{originalMessage}} - -=== UNSERE ANTWORT === -{{adminResponse}} - -Wenn Sie weitere Fragen haben oder zusätzliche Hilfe benötigen, zögern Sie bitte nicht, uns erneut zu kontaktieren. - -Vielen Dank, dass Sie sich für {{companyName}} entschieden haben! - -Für weitere Unterstützung kontaktieren Sie uns unter {{supportEmail}} -Dies ist eine automatische Antwort. Bitte antworten Sie nicht direkt auf diese E-Mail. - ---- -{{companyName}} Support-Team diff --git a/SerpentRace_Backend/src/Templates/contact-response-hu.html b/SerpentRace_Backend/src/Templates/contact-response-hu.html deleted file mode 100644 index cb9bff77..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response-hu.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - {{companyName}} - Válasz az Ön {{contactTypeString}} üzenetére - - - - - - diff --git a/SerpentRace_Backend/src/Templates/contact-response-hu.txt b/SerpentRace_Backend/src/Templates/contact-response-hu.txt deleted file mode 100644 index 5433961b..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response-hu.txt +++ /dev/null @@ -1,21 +0,0 @@ -{{companyName}} - Válasz az Ön {{contactTypeString}} üzenetére - -Kedves {{contactName}}! - -Köszönjük, hogy kapcsolatba lépett velünk! Átnéztük az Ön üzenetét és csapatunk az alábbi választ készítette. - -=== AZ ÖN EREDETI ÜZENETE ({{contactTypeString}}) === -{{originalMessage}} - -=== VÁLASZUNK === -{{adminResponse}} - -Ha további kérdése van vagy további segítségre van szüksége, kérjük, ne habozzon kapcsolatba lépni velünk újra. - -Köszönjük, hogy a {{companyName}} szolgáltatásait választotta! - -További támogatásért lépjen kapcsolatba velünk a {{supportEmail}} címen -Ez egy automatikus válasz. Kérjük, ne válaszoljon közvetlenül erre az e-mailre. - ---- -{{companyName}} Támogatási Csapat diff --git a/SerpentRace_Backend/src/Templates/contact-response.html b/SerpentRace_Backend/src/Templates/contact-response.html deleted file mode 100644 index b5f6fc89..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - {{companyName}} - Response to Your {{contactTypeString}} - - - - - - diff --git a/SerpentRace_Backend/src/Templates/contact-response.txt b/SerpentRace_Backend/src/Templates/contact-response.txt deleted file mode 100644 index cdf4a016..00000000 --- a/SerpentRace_Backend/src/Templates/contact-response.txt +++ /dev/null @@ -1,21 +0,0 @@ -{{companyName}} - Response to Your {{contactTypeString}} - -Hello {{contactName}}, - -Thank you for contacting us! We've reviewed your message and our team has provided a response below. - -=== YOUR ORIGINAL MESSAGE ({{contactTypeString}}) === -{{originalMessage}} - -=== OUR RESPONSE === -{{adminResponse}} - -If you have any additional questions or need further assistance, please don't hesitate to contact us again. - -Thank you for choosing {{companyName}}! - -For additional support, contact us at {{supportEmail}} -This is an automated response. Please do not reply directly to this email. - ---- -{{companyName}} Support Team diff --git a/SerpentRace_Backend/src/Templates/password-reset-de.html b/SerpentRace_Backend/src/Templates/password-reset-de.html deleted file mode 100644 index ff5ee585..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset-de.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - SerpentRace - Passwort zurücksetzen - - - - - - diff --git a/SerpentRace_Backend/src/Templates/password-reset-de.txt b/SerpentRace_Backend/src/Templates/password-reset-de.txt deleted file mode 100644 index 86210ee6..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset-de.txt +++ /dev/null @@ -1,44 +0,0 @@ -🐍 {{ companyName }} - Passwort zurücksetzen -=============================================== - -Hallo {{ userName }}! - -Wir haben eine Anfrage zum Zurücksetzen Ihres Passworts für Ihr {{ companyName }} Konto erhalten. - -Wenn Sie diese Anfrage gestellt haben, verwenden Sie den folgenden Link, um Ihr Passwort zurückzusetzen: - -PASSWORT-RESET-LINK: -{{ resetUrl }} - -RESET-TOKEN: -{{ resetToken }} - -Sie können entweder den obigen Link oder das Reset-Token verwenden, um Ihr Passwort zurückzusetzen. - -WICHTIGE SICHERHEITSINFORMATIONEN: -🚨 Dieser Passwort-Reset-Link läuft aus Sicherheitsgründen in 1 Stunde ab -🚨 Falls Sie keine Passwort-Zurücksetzung angefordert haben, ignorieren Sie diese E-Mail bitte und Ihr Passwort bleibt unverändert -🚨 Teilen Sie Ihr Reset-Token niemals mit anderen -🚨 {{ companyName }} wird Sie niemals per E-Mail nach Ihrem Passwort fragen - -SICHERHEITSTIPPS FÜR IHR NEUES PASSWORT: -💡 Verwenden Sie mindestens 8 Zeichen -💡 Verwenden Sie Groß- und Kleinbuchstaben -💡 Fügen Sie Zahlen und Sonderzeichen hinzu -💡 Verwenden Sie keine Passwörter von anderen Konten wieder -💡 Erwägen Sie die Verwendung eines Passwort-Managers - -DIESE ZURÜCKSETZUNG NICHT ANGEFORDERT? -Falls Sie keine Passwort-Zurücksetzung angefordert haben, ist Ihr Konto weiterhin sicher. Sie können diese E-Mail getrost ignorieren. -Falls Sie jedoch Bedenken bezüglich unbefugten Zugriffs haben, kontaktieren Sie bitte umgehend unser Support-Team. - -BENÖTIGEN SIE HILFE? -Falls Sie Sicherheitsbedenken haben oder Unterstützung benötigen, kontaktieren Sie unser Support-Team unter {{ supportEmail }} - -Zu Ihrer Sicherheit können wir Sie bitten, Ihre Identität zu verifizieren, wenn Sie den Support kontaktieren. - ---- -Diese E-Mail wurde vom {{ companyName }} Sicherheitsteam gesendet -Dies ist eine automatische Nachricht, bitte antworten Sie nicht auf diese E-Mail. - -© 2025 {{ companyName }}. Alle Rechte vorbehalten. diff --git a/SerpentRace_Backend/src/Templates/password-reset-hu.html b/SerpentRace_Backend/src/Templates/password-reset-hu.html deleted file mode 100644 index e2fef7a2..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset-hu.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - SerpentRace - Jelszó visszaállítás kérése - - - - - - diff --git a/SerpentRace_Backend/src/Templates/password-reset-hu.txt b/SerpentRace_Backend/src/Templates/password-reset-hu.txt deleted file mode 100644 index f4e8002c..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset-hu.txt +++ /dev/null @@ -1,44 +0,0 @@ -🐍 {{ companyName }} - Jelszó visszaállítás kérése -=============================================== - -Üdvözöljük {{ userName }}! - -Kérést kaptunk a {{ companyName }} fiókjához tartozó jelszó visszaállítására. - -Ha Ön küldte ezt a kérést, használja az alábbi linket a jelszó visszaállításához: - -JELSZÓ VISSZAÁLLÍTÁSI LINK: -{{ resetUrl }} - -VISSZAÁLLÍTÁSI TOKEN: -{{ resetToken }} - -Használhatja a fenti linket vagy a visszaállítási tokent a jelszava visszaállításához. - -FONTOS BIZTONSÁGI INFORMÁCIÓK: -🚨 Ez a jelszó-visszaállítási link biztonsági okokból 1 óra múlva lejár -🚨 Ha Ön nem kért jelszó visszaállítást, kérjük, hagyja figyelmen kívül ezt az e-mailt, és jelszava változatlan marad -🚨 Soha ne ossza meg a visszaállítási tokenjét senkivel -🚨 A {{ companyName }} soha nem fogja e-mailben kérni az Ön jelszavát - -BIZTONSÁGI TIPPEK AZ ÚJ JELSZAVÁHOZ: -💡 Használjon legalább 8 karaktert -💡 Használjon kis- és nagybetűket -💡 Adjon hozzá számokat és speciális karaktereket -💡 Ne használja újra más fiókok jelszavait -💡 Fontolja meg egy jelszókezelő használatát - -NEM ÖN KÉRTE EZT A VISSZAÁLLÍTÁST? -Ha Ön nem kért jelszó visszaállítást, fiókja továbbra is biztonságos. Nyugodtan figyelmen kívül hagyhatja ezt az e-mailt. -Azonban, ha aggodalmai vannak a jogosulatlan hozzáféréssel kapcsolatban, kérjük, azonnal lépjen kapcsolatba ügyfélszolgálatunkkal. - -SEGÍTSÉGRE VAN SZÜKSÉGE? -Ha biztonsági aggályai vannak vagy segítségre van szüksége, lépjen kapcsolatba ügyfélszolgálatunkkal: {{ supportEmail }} - -Biztonsága érdekében megkérhetjük, hogy igazolja személyazonosságát, amikor kapcsolatba lép ügyfélszolgálatunkkal. - ---- -Ezt az e-mailt a {{ companyName }} Biztonsági Csapata küldte -Ez egy automatikus üzenet, kérjük, ne válaszoljon erre az e-mailre. - -© 2025 {{ companyName }}. Minden jog fenntartva. diff --git a/SerpentRace_Backend/src/Templates/password-reset.html b/SerpentRace_Backend/src/Templates/password-reset.html deleted file mode 100644 index 2b734b4b..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - SerpentRace - Password Reset Request - - - - - - diff --git a/SerpentRace_Backend/src/Templates/password-reset.txt b/SerpentRace_Backend/src/Templates/password-reset.txt deleted file mode 100644 index 1f28794a..00000000 --- a/SerpentRace_Backend/src/Templates/password-reset.txt +++ /dev/null @@ -1,44 +0,0 @@ -🐍 {{ companyName }} - Password Reset Request -=============================================== - -Hello {{ userName }}! - -We received a request to reset your password for your {{ companyName }} account. - -If you made this request, use the link below to reset your password: - -PASSWORD RESET LINK: -{{ resetUrl }} - -RESET TOKEN: -{{ resetToken }} - -You can use either the link above or the reset token to reset your password. - -IMPORTANT SECURITY INFORMATION: -🚨 This password reset link will expire in 1 hour for your security -🚨 If you didn't request a password reset, please ignore this email and your password will remain unchanged -🚨 Never share your reset token with anyone -🚨 {{ companyName }} will never ask for your password via email - -SECURITY TIPS FOR YOUR NEW PASSWORD: -💡 Use at least 8 characters -💡 Include uppercase and lowercase letters -💡 Add numbers and special characters -💡 Don't reuse passwords from other accounts -💡 Consider using a password manager - -DIDN'T REQUEST THIS RESET? -If you didn't request a password reset, your account is still secure. You can safely ignore this email. -However, if you're concerned about unauthorized access, please contact our support team immediately. - -NEED HELP? -If you have security concerns or need assistance, contact our support team at {{ supportEmail }} - -For your security, we may ask you to verify your identity when contacting support. - ---- -This email was sent by {{ companyName }} Security Team -This is an automated message, please do not reply to this email. - -© 2025 {{ companyName }}. All rights reserved. diff --git a/SerpentRace_Backend/src/Templates/verification-de.html b/SerpentRace_Backend/src/Templates/verification-de.html deleted file mode 100644 index 32459a20..00000000 --- a/SerpentRace_Backend/src/Templates/verification-de.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - SerpentRace - Konto verifizieren - - - - - - diff --git a/SerpentRace_Backend/src/Templates/verification-de.txt b/SerpentRace_Backend/src/Templates/verification-de.txt deleted file mode 100644 index ea50afb8..00000000 --- a/SerpentRace_Backend/src/Templates/verification-de.txt +++ /dev/null @@ -1,36 +0,0 @@ -🐍 {{ companyName }} - Konto verifizieren -=============================================== - -Hallo {{ userName }}! - -Willkommen bei {{ companyName }}! Wir freuen uns, dass Sie unserer Gaming-Community beigetreten sind. - -Um Ihre Registrierung abzuschließen und Ihr Konto zu nutzen, verifizieren Sie bitte Ihre E-Mail-Adresse. - -VERIFIZIERUNGSLINK: -{{ verificationUrl }} - -VERIFIZIERUNGSTOKEN: -{{ verificationToken }} - -Sie können entweder den obigen Link oder das Verifizierungstoken verwenden, um Ihr Konto zu verifizieren. - -SICHERHEITSHINWEIS: -⚠️ Dieser Verifizierungslink läuft in 24 Stunden ab -⚠️ Falls Sie kein Konto bei {{ companyName }} erstellt haben, ignorieren Sie diese E-Mail bitte -⚠️ Teilen Sie Ihren Verifizierungstoken niemals mit anderen - -Nach der Verifizierung können Sie: -✨ Ihre Spieldecks erstellen und verwalten -🎮 An Gaming-Turnieren und Wettbewerben teilnehmen -👥 Sich mit anderen Spielern in Ihrer Organisation verbinden -📊 Ihre Gaming-Statistiken und Fortschritte verfolgen - -BENÖTIGEN SIE HILFE? -Falls Sie Fragen haben oder auf Probleme stoßen, kontaktieren Sie bitte unser Support-Team unter {{ supportEmail }} - ---- -Diese E-Mail wurde von {{ companyName }} gesendet -Dies ist eine automatische Nachricht, bitte antworten Sie nicht auf diese E-Mail. - -© 2025 {{ companyName }}. Alle Rechte vorbehalten. diff --git a/SerpentRace_Backend/src/Templates/verification-hu.html b/SerpentRace_Backend/src/Templates/verification-hu.html deleted file mode 100644 index e6cc0214..00000000 --- a/SerpentRace_Backend/src/Templates/verification-hu.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - SerpentRace - Fiók megerősítése - - - - - - diff --git a/SerpentRace_Backend/src/Templates/verification-hu.txt b/SerpentRace_Backend/src/Templates/verification-hu.txt deleted file mode 100644 index 4519a90a..00000000 --- a/SerpentRace_Backend/src/Templates/verification-hu.txt +++ /dev/null @@ -1,36 +0,0 @@ -🐍 {{ companyName }} - Fiók megerősítése -=============================================== - -Üdvözöljük {{ userName }}! - -Üdvözöljük a {{ companyName }} közösségében! Örülünk, hogy csatlakozott hozzánk játékosközösségünkhöz. - -A regisztráció befejezéséhez és fiókja használatbavételéhez kérjük, erősítse meg e-mail címét. - -MEGERŐSÍTÉSI LINK: -{{ verificationUrl }} - -MEGERŐSÍTÉSI TOKEN: -{{ verificationToken }} - -Használhatja a fenti linket vagy a megerősítési tokent fiókja megerősítéséhez. - -BIZTONSÁGI FIGYELMEZTETÉS: -⚠️ Ez a megerősítési link 24 óra múlva lejár -⚠️ Ha Ön nem hozott létre fiókot a {{ companyName }}-nál, kérjük, hagyja figyelmen kívül ezt az e-mailt -⚠️ Soha ne ossza meg a megerősítési tokenjét senkivel - -A megerősítés után lehetősége lesz: -✨ Játékcsomagok létrehozására és kezelésére -🎮 Játékversenyeken és bajnokságokon való részvételre -👥 Kapcsolatfelvételre szervezetében lévő más játékosokkal -📊 Játékstatisztikák és fejlődés nyomon követésére - -SEGÍTSÉGRE VAN SZÜKSÉGE? -Ha kérdései vannak vagy problémákba ütközik, kérjük, lépjen kapcsolatba ügyfélszolgálatunkkal: {{ supportEmail }} - ---- -Ezt az e-mailt a {{ companyName }} küldte -Ez egy automatikus üzenet, kérjük, ne válaszoljon erre az e-mailre. - -© 2025 {{ companyName }}. Minden jog fenntartva. diff --git a/SerpentRace_Backend/src/Templates/verification.html b/SerpentRace_Backend/src/Templates/verification.html deleted file mode 100644 index f5cb0500..00000000 --- a/SerpentRace_Backend/src/Templates/verification.html +++ /dev/null @@ -1,204 +0,0 @@ - - - - - - SerpentRace - Verify Your Account - - - - - - diff --git a/SerpentRace_Backend/src/Templates/verification.txt b/SerpentRace_Backend/src/Templates/verification.txt deleted file mode 100644 index 3986b3a6..00000000 --- a/SerpentRace_Backend/src/Templates/verification.txt +++ /dev/null @@ -1,36 +0,0 @@ -🐍 {{ companyName }} - Account Verification -=============================================== - -Hello {{ userName }}! - -Welcome to {{ companyName }}! We're excited to have you join our gaming community. - -To complete your registration and start using your account, please verify your email address. - -VERIFICATION LINK: -{{ verificationUrl }} - -VERIFICATION TOKEN: -{{ verificationToken }} - -You can use either the link above or the verification token to verify your account. - -SECURITY NOTICE: -⚠️ This verification link will expire in 24 hours -⚠️ If you didn't create an account with {{ companyName }}, please ignore this email -⚠️ Never share your verification token with anyone - -Once verified, you'll be able to: -✨ Create and manage your game decks -🎮 Join gaming tournaments and competitions -👥 Connect with other players in your organization -📊 Track your gaming statistics and progress - -NEED HELP? -If you have any questions or encounter issues, please contact our support team at {{ supportEmail }} - ---- -This email was sent by {{ companyName }} -This is an automated message, please do not reply to this email. - -© 2025 {{ companyName }}. All rights reserved. diff --git a/SerpentRace_Backend/test-org-auth.js b/SerpentRace_Backend/test-org-auth.js deleted file mode 100644 index 4e627ad2..00000000 --- a/SerpentRace_Backend/test-org-auth.js +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env node - -/** - * Test script for Organization Authentication functionality - * This script tests the new organization authentication features: - * 1. Get organization login URL - * 2. Process third-party authentication callback - * 3. Login with organization reauthentication check - */ - -const { container } = require('./dist/Application/Services/DIContainer.js'); - -async function testOrganizationAuth() { - console.log('🧪 Testing Organization Authentication Functionality\n'); - - try { - // Test 1: Get Organization Login URL - console.log('1️⃣ Testing Get Organization Login URL Query Handler'); - const getUrlHandler = container.getOrganizationLoginUrlQueryHandler; - console.log('✅ Handler instantiated successfully'); - - // Test 2: Process Organization Auth Callback - console.log('2️⃣ Testing Process Organization Auth Callback Command Handler'); - const callbackHandler = container.processOrgAuthCallbackCommandHandler; - console.log('✅ Handler instantiated successfully'); - - // Test 3: Enhanced Login Handler with Organization Repository - console.log('3️⃣ Testing Enhanced Login Handler'); - const loginHandler = container.loginCommandHandler; - console.log('✅ Enhanced login handler instantiated successfully'); - - console.log('\n🎉 All Organization Authentication components initialized successfully!'); - console.log('\n📋 Summary of new functionality:'); - console.log(' • GET /api/organizations/:orgId/login-url - Get organization third-party login URL'); - console.log(' • POST /api/organizations/auth-callback - Process third-party authentication result'); - console.log(' • Enhanced login response includes organization reauthentication requirements'); - console.log(' • Users must reauthenticate with organization if last login > 1 month ago'); - - } catch (error) { - console.error('❌ Error testing organization authentication:', error.message); - process.exit(1); - } -} - -// Run the test -testOrganizationAuth(); diff --git a/SerpentRace_Backend/tests/Application/Chat/ChatMessagingSystem.test.ts b/SerpentRace_Backend/tests/Application/Chat/ChatMessagingSystem.test.ts deleted file mode 100644 index 1f7ee565..00000000 --- a/SerpentRace_Backend/tests/Application/Chat/ChatMessagingSystem.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals'; -import { AppDataSource } from '../../../src/Infrastructure/ormconfig'; -import { ChatRepository } from '../../../src/Infrastructure/Repository/ChatRepository'; -import { ChatArchiveRepository } from '../../../src/Infrastructure/Repository/ChatArchiveRepository'; -import { UserRepository } from '../../../src/Infrastructure/Repository/UserRepository'; -import { ChatType } from '../../../src/Domain/Chat/ChatAggregate'; -import { UserState } from '../../../src/Domain/User/UserAggregate'; -import { v4 as uuidv4 } from 'uuid'; - -describe('Chat Messaging System', () => { - let chatRepository: ChatRepository; - let chatArchiveRepository: ChatArchiveRepository; - let userRepository: UserRepository; - - let testUser1: any; - let testUser2: any; - let testPremiumUser: any; - - beforeAll(async () => { - if (!AppDataSource.isInitialized) { - await AppDataSource.initialize(); - } - - chatRepository = new ChatRepository(); - chatArchiveRepository = new ChatArchiveRepository(); - userRepository = new UserRepository(); - }); - - beforeEach(async () => { - // Create test users - testUser1 = await userRepository.create({ - username: `testuser1_${Date.now()}`, - email: `test1_${Date.now()}@example.com`, - password: 'hashedpassword', - fname: 'Test', - lname: 'User1', - type: 'regular', - state: UserState.VERIFIED_REGULAR - }); - - testUser2 = await userRepository.create({ - username: `testuser2_${Date.now()}`, - email: `test2_${Date.now()}@example.com`, - password: 'hashedpassword', - fname: 'Test', - lname: 'User2', - type: 'regular', - state: UserState.VERIFIED_REGULAR - }); - - testPremiumUser = await userRepository.create({ - username: `premiumuser_${Date.now()}`, - email: `premium_${Date.now()}@example.com`, - password: 'hashedpassword', - fname: 'Premium', - lname: 'User', - type: 'premium', - state: UserState.VERIFIED_PREMIUM - }); - }); - - afterAll(async () => { - if (AppDataSource.isInitialized) { - await AppDataSource.destroy(); - } - }); - - describe('Direct Chat Creation', () => { - it('should create a direct chat between two users', async () => { - const chat = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - expect(chat).toBeDefined(); - expect(chat.type).toBe(ChatType.DIRECT); - expect(chat.users).toEqual([testUser1.id, testUser2.id]); - expect(chat.messages).toEqual([]); - }); - }); - - describe('Group Chat Creation', () => { - it('should create a group chat', async () => { - const chat = await chatRepository.create({ - type: ChatType.GROUP, - name: 'Test Group', - createdBy: testPremiumUser.id, - users: [testPremiumUser.id, testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - expect(chat).toBeDefined(); - expect(chat.type).toBe(ChatType.GROUP); - expect(chat.name).toBe('Test Group'); - expect(chat.createdBy).toBe(testPremiumUser.id); - expect(chat.users.length).toBe(3); - }); - }); - - describe('Game Chat Creation', () => { - it('should create a game chat', async () => { - const gameId = uuidv4(); - - const chat = await chatRepository.create({ - type: ChatType.GAME, - name: 'Test Game Chat', - gameId: gameId, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - expect(chat).toBeDefined(); - expect(chat.type).toBe(ChatType.GAME); - expect(chat.gameId).toBe(gameId); - expect(chat.name).toBe('Test Game Chat'); - }); - - it('should find game chat by game id', async () => { - const gameId = uuidv4(); - - await chatRepository.create({ - type: ChatType.GAME, - name: 'Test Game Chat', - gameId: gameId, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - const foundChat = await chatRepository.findByGameId(gameId); - expect(foundChat).toBeDefined(); - expect(foundChat!.gameId).toBe(gameId); - }); - }); - - describe('Message Management', () => { - it('should add and retrieve messages', async () => { - const chat = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - const message = { - id: uuidv4(), - date: new Date(), - userid: testUser1.id, - text: 'Hello, this is a test message!' - }; - - await chatRepository.update(chat.id, { - messages: [message], - lastActivity: new Date() - }); - - const updatedChat = await chatRepository.findById(chat.id); - expect(updatedChat!.messages).toHaveLength(1); - expect(updatedChat!.messages[0].text).toBe('Hello, this is a test message!'); - expect(updatedChat!.messages[0].userid).toBe(testUser1.id); - }); - }); - - describe('Chat Archiving', () => { - it('should archive a chat with messages', async () => { - const message = { - id: uuidv4(), - date: new Date(), - userid: testUser1.id, - text: 'Message to be archived' - }; - - const chat = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [message], - lastActivity: new Date() - }); - - const archive = await chatRepository.archiveChat(chat); - - expect(archive).toBeDefined(); - expect(archive.chatId).toBe(chat.id); - expect(archive.archivedMessages).toHaveLength(1); - expect(archive.archivedMessages[0].text).toBe('Message to be archived'); - - // Check that chat messages were cleared - const archivedChat = await chatRepository.findById(chat.id); - expect(archivedChat!.messages).toEqual([]); - expect(archivedChat!.archiveDate).toBeDefined(); - }); - - it('should retrieve archived chat', async () => { - const message = { - id: uuidv4(), - date: new Date(), - userid: testUser1.id, - text: 'Archived message' - }; - - const chat = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [message], - lastActivity: new Date() - }); - - await chatRepository.archiveChat(chat); - - const archive = await chatRepository.getArchivedChat(chat.id); - expect(archive).toBeDefined(); - expect(archive!.archivedMessages).toHaveLength(1); - expect(archive!.archivedMessages[0].text).toBe('Archived message'); - }); - }); - - describe('Chat Queries', () => { - it('should find chats by user id', async () => { - const chat1 = await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - const chat2 = await chatRepository.create({ - type: ChatType.GROUP, - name: 'Test Group', - createdBy: testPremiumUser.id, - users: [testPremiumUser.id, testUser1.id], - messages: [], - lastActivity: new Date() - }); - - const userChats = await chatRepository.findByUserId(testUser1.id); - expect(userChats.length).toBeGreaterThanOrEqual(2); - - const chatIds = userChats.map(c => c.id); - expect(chatIds).toContain(chat1.id); - expect(chatIds).toContain(chat2.id); - }); - - it('should find active chats for user', async () => { - await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: new Date() - }); - - const activeChats = await chatRepository.findActiveChatsForUser(testUser1.id); - expect(activeChats.length).toBeGreaterThanOrEqual(1); - - // All returned chats should be active - activeChats.forEach(chat => { - expect(chat.users).toContain(testUser1.id); - }); - }); - - it('should find inactive chats', async () => { - const oldDate = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago - - await chatRepository.create({ - type: ChatType.DIRECT, - users: [testUser1.id, testUser2.id], - messages: [], - lastActivity: oldDate - }); - - const inactiveChats = await chatRepository.findInactiveChats(60); // 60 minutes - expect(inactiveChats.length).toBeGreaterThanOrEqual(1); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Contact/commands/ContactCommandHandlers.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/Contact/commands/ContactCommandHandlers.comprehensive.test.ts deleted file mode 100644 index 734298e8..00000000 --- a/SerpentRace_Backend/tests/Application/Contact/commands/ContactCommandHandlers.comprehensive.test.ts +++ /dev/null @@ -1,402 +0,0 @@ -import { CreateContactCommandHandler } from '../../../../src/Application/Contact/commands/CreateContactCommandHandler'; -import { UpdateContactCommandHandler } from '../../../../src/Application/Contact/commands/UpdateContactCommandHandler'; -import { DeleteContactCommandHandler } from '../../../../src/Application/Contact/commands/DeleteContactCommandHandler'; -import { CreateContactCommand } from '../../../../src/Application/Contact/commands/CreateContactCommand'; -import { UpdateContactCommand } from '../../../../src/Application/Contact/commands/UpdateContactCommand'; -import { DeleteContactCommand } from '../../../../src/Application/Contact/commands/DeleteContactCommand'; -import { ContactType, ContactState } from '../../../../src/Domain/Contact/ContactAggregate'; -import { createMockContactRepository, createMockContact } from '../../../testUtils'; - -describe('Contact Command Handlers - Comprehensive', () => { - let mockContactRepository: ReturnType; - - beforeEach(() => { - mockContactRepository = createMockContactRepository(); - }); - - describe('CreateContactCommandHandler', () => { - let handler: CreateContactCommandHandler; - - beforeEach(() => { - handler = new CreateContactCommandHandler(mockContactRepository); - }); - - it('should create contact successfully with all fields', async () => { - // Arrange - const mockContactData = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'John Doe', - email: 'john@example.com', - userid: '123e4567-e89b-12d3-a456-426614174000', - type: ContactType.QUESTION, - txt: 'Test question', - state: ContactState.ACTIVE - }); - - mockContactRepository.create.mockResolvedValue(mockContactData); - - const command: CreateContactCommand = { - name: 'John Doe', - email: 'john@example.com', - userid: '123e4567-e89b-12d3-a456-426614174000', - type: ContactType.QUESTION, - txt: 'Test question' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Returns ShortContactDto - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'John Doe', - email: 'john@example.com', - type: ContactType.QUESTION, - state: ContactState.ACTIVE, - createDate: expect.any(Date) - }); - expect(mockContactRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'John Doe', - email: 'john@example.com', - userid: '123e4567-e89b-12d3-a456-426614174000', - type: ContactType.QUESTION, - txt: 'Test question', - state: ContactState.ACTIVE - }) - ); - }); - - it('should create contact without userid (anonymous)', async () => { - // Arrange - const mockContactData = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440001', - name: 'Anonymous User', - email: 'anon@example.com', - userid: null, - type: ContactType.BUG, - txt: 'Bug report', - state: ContactState.ACTIVE - }); - - mockContactRepository.create.mockResolvedValue(mockContactData); - - const command: CreateContactCommand = { - name: 'Anonymous User', - email: 'anon@example.com', - type: ContactType.BUG, - txt: 'Bug report' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440001', - name: 'Anonymous User', - email: 'anon@example.com', - type: ContactType.BUG, - state: ContactState.ACTIVE, - createDate: expect.any(Date) - }); - expect(mockContactRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - userid: null - }) - ); - }); - - it('should create contact with different contact types', async () => { - const testCases = [ - { type: ContactType.BUG, description: 'Bug report' }, - { type: ContactType.PROBLEM, description: 'Problem report' }, - { type: ContactType.QUESTION, description: 'Question' }, - { type: ContactType.SALES, description: 'Sales inquiry' }, - { type: ContactType.OTHER, description: 'Other inquiry' } - ]; - - for (const testCase of testCases) { - // Arrange - const mockContactData = createMockContact({ - type: testCase.type, - txt: testCase.description - }); - - mockContactRepository.create.mockResolvedValue(mockContactData); - - const command: CreateContactCommand = { - name: 'Test User', - email: 'test@example.com', - type: testCase.type, - txt: testCase.description - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result.type).toBe(testCase.type); - expect(mockContactRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - txt: testCase.description - }) - ); - } - }); - - it('should handle database errors', async () => { - // Arrange - const command: CreateContactCommand = { - name: 'Error User', - email: 'error@example.com', - type: ContactType.QUESTION, - txt: 'This will cause an error' - }; - - mockContactRepository.create.mockRejectedValue(new Error('Database error')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create contact'); - }); - - it('should handle non-Error exceptions', async () => { - // Arrange - const command: CreateContactCommand = { - name: 'Exception User', - email: 'exception@example.com', - type: ContactType.QUESTION, - txt: 'This will cause an exception' - }; - - mockContactRepository.create.mockRejectedValue('String error'); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create contact'); - }); - }); - - describe('UpdateContactCommandHandler', () => { - let handler: UpdateContactCommandHandler; - - beforeEach(() => { - handler = new UpdateContactCommandHandler(mockContactRepository); - }); - - it('should update contact with admin response', async () => { - // Arrange - const existingContact = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000', - adminResponse: null, - state: ContactState.ACTIVE - }); - - const updatedContact = createMockContact({ - ...existingContact, - adminResponse: 'Thank you for your inquiry', - state: ContactState.RESOLVED, - responseDate: new Date(), - respondedBy: 'admin123' - }); - - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.update.mockResolvedValue(updatedContact); - - const command: UpdateContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - adminResponse: 'Thank you for your inquiry' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Returns DetailContactDto - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: expect.any(String), - email: expect.any(String), - userid: expect.any(String), - type: expect.any(Number), - txt: expect.any(String), - state: ContactState.RESOLVED, - createDate: expect.any(Date), - updateDate: expect.any(Date), - adminResponse: 'Thank you for your inquiry', - responseDate: expect.any(Date), - respondedBy: 'admin123' - }); - }); - - it('should update contact state', async () => { - // Arrange - const existingContact = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000', - state: ContactState.ACTIVE - }); - - const updatedContact = createMockContact({ - ...existingContact, - state: ContactState.RESOLVED - }); - - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.update.mockResolvedValue(updatedContact); - - const command: UpdateContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - state: ContactState.RESOLVED - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result.state).toBe(ContactState.RESOLVED); - }); - - it('should throw error when contact not found', async () => { - // Arrange - mockContactRepository.findById.mockResolvedValue(null); - - const command: UpdateContactCommand = { - id: 'non-existent-id', - adminResponse: 'Response' - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Contact not found'); - }); - - it('should handle repository errors during update', async () => { - // Arrange - const existingContact = createMockContact(); - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.update.mockRejectedValue(new Error('Database error')); - - const command: UpdateContactCommand = { - id: 'existing-id', - adminResponse: 'Response' - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to update contact'); - }); - }); - - describe('DeleteContactCommandHandler', () => { - let handler: DeleteContactCommandHandler; - - beforeEach(() => { - handler = new DeleteContactCommandHandler(mockContactRepository); - }); - - it('should perform soft delete successfully', async () => { - // Arrange - const existingContact = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000' - }); - - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.softDelete.mockResolvedValue(null); - - const command: DeleteContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - hard: false - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockContactRepository.findById).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockContactRepository.softDelete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockContactRepository.delete).not.toHaveBeenCalled(); - }); - - it('should perform hard delete successfully', async () => { - // Arrange - const existingContact = createMockContact({ - id: '550e8400-e29b-41d4-a716-446655440000' - }); - - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.delete.mockResolvedValue(true); - - const command: DeleteContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - hard: true - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockContactRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockContactRepository.softDelete).not.toHaveBeenCalled(); - }); - - it('should default to soft delete when hard flag not specified', async () => { - // Arrange - const existingContact = createMockContact(); - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.softDelete.mockResolvedValue(null); - - const command: DeleteContactCommand = { - id: '550e8400-e29b-41d4-a716-446655440000' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockContactRepository.softDelete).toHaveBeenCalled(); - expect(mockContactRepository.delete).not.toHaveBeenCalled(); - }); - - it('should throw error when contact not found', async () => { - // Arrange - mockContactRepository.findById.mockResolvedValue(null); - - const command: DeleteContactCommand = { - id: 'non-existent-id', - hard: false - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Contact not found'); - }); - - it('should handle repository errors during deletion', async () => { - // Arrange - const existingContact = createMockContact(); - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.softDelete.mockRejectedValue(new Error('Database error')); - - const command: DeleteContactCommand = { - id: 'existing-id', - hard: false - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to delete contact'); - }); - - it('should handle hard delete repository errors', async () => { - // Arrange - const existingContact = createMockContact(); - mockContactRepository.findById.mockResolvedValue(existingContact); - mockContactRepository.delete.mockRejectedValue(new Error('Database error')); - - const command: DeleteContactCommand = { - id: 'existing-id', - hard: true - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to delete contact'); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/DTOs/Mappers/ContactMapper.test.ts b/SerpentRace_Backend/tests/Application/DTOs/Mappers/ContactMapper.test.ts deleted file mode 100644 index 0c8110c9..00000000 --- a/SerpentRace_Backend/tests/Application/DTOs/Mappers/ContactMapper.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { ContactMapper } from '../../../../src/Application/DTOs/Mappers/ContactMapper'; -import { ContactType, ContactState } from '../../../../src/Domain/Contact/ContactAggregate'; - -describe('ContactMapper', () => { - const createMockContact = (overrides: any = {}) => ({ - id: 'contact-123', - name: 'John Doe', - email: 'john.doe@example.com', - userid: 'user-456', - type: ContactType.QUESTION, - txt: 'This is a test contact message.', - state: ContactState.ACTIVE, - createDate: new Date('2024-01-01'), - updateDate: new Date('2024-01-02'), - adminResponse: null, - responseDate: null, - respondedBy: null, - ...overrides - }); - - describe('toShortDto', () => { - it('should map ContactAggregate to ShortContactDto correctly', () => { - // Arrange - const contact = createMockContact(); - - // Act - const result = ContactMapper.toShortDto(contact); - - // Assert - expect(result).toEqual({ - id: 'contact-123', - name: 'John Doe', - email: 'john.doe@example.com', - type: ContactType.QUESTION, - createDate: new Date('2024-01-01'), - state: ContactState.ACTIVE, - }); - }); - - it('should handle different contact types', () => { - // Arrange - const bugContact = createMockContact({ - id: 'bug-contact', - type: ContactType.BUG, - name: 'Bug Reporter' - }); - - // Act - const result = ContactMapper.toShortDto(bugContact); - - // Assert - expect(result.type).toBe(ContactType.BUG); - expect(result.name).toBe('Bug Reporter'); - }); - }); - - describe('toDetailDto', () => { - it('should map ContactAggregate to DetailContactDto correctly', () => { - // Arrange - const contact = createMockContact(); - - // Act - const result = ContactMapper.toDetailDto(contact); - - // Assert - expect(result).toEqual({ - id: 'contact-123', - name: 'John Doe', - email: 'john.doe@example.com', - userid: 'user-456', - type: ContactType.QUESTION, - txt: 'This is a test contact message.', - state: ContactState.ACTIVE, - createDate: new Date('2024-01-01'), - updateDate: new Date('2024-01-02'), - adminResponse: null, - responseDate: null, - respondedBy: null, - }); - }); - - it('should handle contact with admin response', () => { - // Arrange - const respondedContact = createMockContact({ - adminResponse: 'Thank you for your question. Here is the answer...', - responseDate: new Date('2024-01-03'), - respondedBy: 'admin-789' - }); - - // Act - const result = ContactMapper.toDetailDto(respondedContact); - - // Assert - expect(result.adminResponse).toBe('Thank you for your question. Here is the answer...'); - expect(result.responseDate).toEqual(new Date('2024-01-03')); - expect(result.respondedBy).toBe('admin-789'); - }); - }); - - describe('toShortDtoList', () => { - it('should map array of ContactAggregate to array of ShortContactDto', () => { - // Arrange - const contacts = [ - createMockContact({ id: 'contact-1', name: 'First Contact' }), - createMockContact({ id: 'contact-2', name: 'Second Contact', type: ContactType.BUG }), - createMockContact({ id: 'contact-3', name: 'Third Contact', type: ContactType.SALES }) - ]; - - // Act - const result = ContactMapper.toShortDtoList(contacts); - - // Assert - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ - id: 'contact-1', - name: 'First Contact', - email: 'john.doe@example.com', - type: ContactType.QUESTION, - createDate: new Date('2024-01-01'), - state: ContactState.ACTIVE, - }); - expect(result[1].type).toBe(ContactType.BUG); - expect(result[2].type).toBe(ContactType.SALES); - }); - - it('should handle empty array', () => { - // Arrange - const contacts: any[] = []; - - // Act - const result = ContactMapper.toShortDtoList(contacts); - - // Assert - expect(result).toEqual([]); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/DTOs/Mappers/DeckMapper.test.ts b/SerpentRace_Backend/tests/Application/DTOs/Mappers/DeckMapper.test.ts deleted file mode 100644 index 35a98110..00000000 --- a/SerpentRace_Backend/tests/Application/DTOs/Mappers/DeckMapper.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { DeckMapper } from '../../../../src/Application/DTOs/Mappers/DeckMapper'; -import { Type, CType, State } from '../../../../src/Domain/Deck/DeckAggregate'; - -describe('DeckMapper', () => { - const createMockDeck = (overrides: any = {}) => ({ - id: 'deck-123', - name: 'Test Deck', - type: Type.LUCK, - userid: 'user-123', - creationdate: new Date('2024-01-01'), - cards: [ - { text: 'Test card 1', answer: 'Answer 1' }, - { text: 'Test card 2' } - ], - playedNumber: 5, - ctype: CType.PUBLIC, - updateDate: new Date('2024-01-02'), - state: State.ACTIVE, - organization: null, - user: { username: 'testuser', id: 'user-123', isAdmin: false }, - isEditable: jest.fn().mockReturnValue(true), - ...overrides - }); - - describe('toShortDto', () => { - it('should map DeckAggregate to ShortDeckDto correctly', () => { - // Arrange - const deck = createMockDeck(); - - // Act - const result = DeckMapper.toShortDto(deck); - - // Assert - expect(result).toEqual({ - id: 'deck-123', - name: 'Test Deck', - type: Type.LUCK, - playedNumber: 5, - ctype: CType.PUBLIC - }); - }); - - it('should handle different deck types', () => { - // Arrange - const jokeDeck = createMockDeck({ - id: 'joker-deck', - name: 'Joker Deck', - type: Type.JOKER, - playedNumber: 10 - }); - - // Act - const result = DeckMapper.toShortDto(jokeDeck); - - // Assert - expect(result.type).toBe(Type.JOKER); - expect(result.playedNumber).toBe(10); - }); - - it('should handle private decks', () => { - // Arrange - const privateDeck = createMockDeck({ - ctype: CType.PRIVATE, - playedNumber: 0 - }); - - // Act - const result = DeckMapper.toShortDto(privateDeck); - - // Assert - expect(result.ctype).toBe(CType.PRIVATE); - expect(result.playedNumber).toBe(0); - }); - }); - - describe('toDetailDto', () => { - it('should map DeckAggregate to DetailDeckDto correctly', () => { - // Arrange - const deck = createMockDeck(); - - // Act - const result = DeckMapper.toDetailDto(deck); - - // Assert - expect(result).toEqual({ - id: 'deck-123', - name: 'Test Deck', - type: Type.LUCK, - userid: 'user-123', - creationdate: new Date('2024-01-01'), - cards: [ - { text: 'Test card 1', answer: 'Answer 1' }, - { text: 'Test card 2' } - ], - playedNumber: 5, - ctype: CType.PUBLIC - }); - }); - - it('should handle empty cards array', () => { - // Arrange - const deckWithNoCards = createMockDeck({ - cards: [] - }); - - // Act - const result = DeckMapper.toDetailDto(deckWithNoCards); - - // Assert - expect(result.cards).toEqual([]); - }); - - it('should handle question type deck', () => { - // Arrange - const questionDeck = createMockDeck({ - type: Type.QUESTION, - cards: [ - { text: 'Question 1?', answer: 'Answer 1' }, - { text: 'Question 2?', answer: null } - ] - }); - - // Act - const result = DeckMapper.toDetailDto(questionDeck); - - // Assert - expect(result.type).toBe(Type.QUESTION); - expect(result.cards).toHaveLength(2); - expect(result.cards[1].answer).toBeNull(); - }); - }); - - describe('toShortDtoList', () => { - it('should map array of DeckAggregate to array of ShortDeckDto', () => { - // Arrange - const decks = [ - createMockDeck({ id: 'deck-1', name: 'First Deck' }), - createMockDeck({ id: 'deck-2', name: 'Second Deck', type: Type.JOKER }), - createMockDeck({ id: 'deck-3', name: 'Third Deck', ctype: CType.PRIVATE }) - ]; - - // Act - const result = DeckMapper.toShortDtoList(decks); - - // Assert - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ - id: 'deck-1', - name: 'First Deck', - type: Type.LUCK, - playedNumber: 5, - ctype: CType.PUBLIC - }); - expect(result[1].type).toBe(Type.JOKER); - expect(result[2].ctype).toBe(CType.PRIVATE); - }); - - it('should handle empty array', () => { - // Arrange - const decks: any[] = []; - - // Act - const result = DeckMapper.toShortDtoList(decks); - - // Assert - expect(result).toEqual([]); - expect(result).toHaveLength(0); - }); - - it('should handle large arrays', () => { - // Arrange - const decks = Array.from({ length: 50 }, (_, i) => - createMockDeck({ - id: `deck-${i + 1}`, - name: `Deck ${i + 1}`, - playedNumber: i - }) - ); - - // Act - const result = DeckMapper.toShortDtoList(decks); - - // Assert - expect(result).toHaveLength(50); - expect(result[0].playedNumber).toBe(0); - expect(result[49].playedNumber).toBe(49); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/DTOs/Mappers/OrganizationMapper.test.ts b/SerpentRace_Backend/tests/Application/DTOs/Mappers/OrganizationMapper.test.ts deleted file mode 100644 index 5dad953d..00000000 --- a/SerpentRace_Backend/tests/Application/DTOs/Mappers/OrganizationMapper.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { OrganizationMapper } from '../../../../src/Application/DTOs/Mappers/OrganizationMapper'; -import { OrganizationState, OrganizationStateType } from '../../../../src/Domain/Organization/OrganizationAggregate'; - -describe('OrganizationMapper', () => { - const createMockOrganization = (overrides: any = {}) => ({ - id: 'org-123', - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactphone: '+1234567890', - contactemail: 'john@test.org', - state: OrganizationState.ACTIVE as OrganizationStateType, - regdate: new Date('2024-01-01'), - updateDate: new Date('2024-01-02'), - url: 'https://test.org', - userinorg: 5, - maxOrganizationalDecks: 10, - users: [ - { id: 'user-1', name: 'User One' }, - { id: 'user-2', name: 'User Two' } - ], - ...overrides - }); - - describe('toShortDto', () => { - it('should map OrganizationAggregate to ShortOrganizationDto correctly', () => { - // Arrange - const org = createMockOrganization(); - - // Act - const result = OrganizationMapper.toShortDto(org); - - // Assert - expect(result).toEqual({ - id: 'org-123', - name: 'Test Organization', - state: OrganizationState.ACTIVE, - userinorg: 5 - }); - }); - - it('should handle different organization states', () => { - // Arrange - const registeredOrg = createMockOrganization({ - state: OrganizationState.REGISTERED, - userinorg: 0 - }); - - // Act - const result = OrganizationMapper.toShortDto(registeredOrg); - - // Assert - expect(result.state).toBe(OrganizationState.REGISTERED); - expect(result.userinorg).toBe(0); - }); - - it('should handle organization with many users', () => { - // Arrange - const orgWithManyUsers = createMockOrganization({ - userinorg: 100 - }); - - // Act - const result = OrganizationMapper.toShortDto(orgWithManyUsers); - - // Assert - expect(result.userinorg).toBe(100); - }); - }); - - describe('toDetailDto', () => { - it('should map OrganizationAggregate to DetailOrganizationDto correctly', () => { - // Arrange - const org = createMockOrganization(); - - // Act - const result = OrganizationMapper.toDetailDto(org); - - // Assert - expect(result).toEqual({ - id: 'org-123', - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactphone: '+1234567890', - contactemail: 'john@test.org', - state: OrganizationState.ACTIVE, - regdate: new Date('2024-01-01'), - updateDate: new Date('2024-01-02'), - url: 'https://test.org', - userinorg: 5, - maxOrganizationalDecks: 10, - users: ['user-1', 'user-2'] - }); - }); - - it('should handle organization without URL', () => { - // Arrange - const orgWithoutUrl = createMockOrganization({ - url: null - }); - - // Act - const result = OrganizationMapper.toDetailDto(orgWithoutUrl); - - // Assert - expect(result.url).toBeNull(); - }); - - it('should handle organization without users', () => { - // Arrange - const orgWithoutUsers = createMockOrganization({ - users: null, - userinorg: 0 - }); - - // Act - const result = OrganizationMapper.toDetailDto(orgWithoutUsers); - - // Assert - expect(result.users).toEqual([]); - expect(result.userinorg).toBe(0); - }); - - it('should handle empty users array', () => { - // Arrange - const orgWithEmptyUsers = createMockOrganization({ - users: [], - userinorg: 0 - }); - - // Act - const result = OrganizationMapper.toDetailDto(orgWithEmptyUsers); - - // Assert - expect(result.users).toEqual([]); - }); - - it('should handle soft deleted organization', () => { - // Arrange - const softDeletedOrg = createMockOrganization({ - state: OrganizationState.SOFT_DELETE - }); - - // Act - const result = OrganizationMapper.toDetailDto(softDeletedOrg); - - // Assert - expect(result.state).toBe(OrganizationState.SOFT_DELETE); - }); - }); - - describe('toShortDtoList', () => { - it('should map array of OrganizationAggregate to array of ShortOrganizationDto', () => { - // Arrange - const orgs = [ - createMockOrganization({ id: 'org-1', name: 'First Org', userinorg: 10 }), - createMockOrganization({ id: 'org-2', name: 'Second Org', state: OrganizationState.REGISTERED }), - createMockOrganization({ id: 'org-3', name: 'Third Org', userinorg: 0 }) - ]; - - // Act - const result = OrganizationMapper.toShortDtoList(orgs); - - // Assert - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ - id: 'org-1', - name: 'First Org', - state: OrganizationState.ACTIVE, - userinorg: 10 - }); - expect(result[1].state).toBe(OrganizationState.REGISTERED); - expect(result[2].userinorg).toBe(0); - }); - - it('should handle empty array', () => { - // Arrange - const orgs: any[] = []; - - // Act - const result = OrganizationMapper.toShortDtoList(orgs); - - // Assert - expect(result).toEqual([]); - expect(result).toHaveLength(0); - }); - - it('should handle large arrays', () => { - // Arrange - const orgs = Array.from({ length: 25 }, (_, i) => - createMockOrganization({ - id: `org-${i + 1}`, - name: `Organization ${i + 1}`, - userinorg: i * 2 - }) - ); - - // Act - const result = OrganizationMapper.toShortDtoList(orgs); - - // Assert - expect(result).toHaveLength(25); - expect(result[0].userinorg).toBe(0); - expect(result[24].userinorg).toBe(48); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/DTOs/Mappers/UserMapper.test.ts b/SerpentRace_Backend/tests/Application/DTOs/Mappers/UserMapper.test.ts deleted file mode 100644 index f9b4789b..00000000 --- a/SerpentRace_Backend/tests/Application/DTOs/Mappers/UserMapper.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { UserMapper } from '../../../../src/Application/DTOs/Mappers/UserMapper'; -import { UserAggregate, UserState } from '../../../../src/Domain/User/UserAggregate'; -import { createMockUser } from '../../../testUtils'; - -describe('UserMapper', () => { - describe('toShortDto', () => { - it('should map UserAggregate to ShortUserDto correctly', () => { - // Arrange - const user = createMockUser({ - id: 'user-123', - username: 'testuser', - email: 'test@example.com', - fname: 'John', - lname: 'Doe', - state: UserState.VERIFIED_REGULAR - }); - - // Act - const result = UserMapper.toShortDto(user); - - // Assert - expect(result).toEqual({ - id: 'user-123', - username: 'testuser', - state: UserState.VERIFIED_REGULAR, - authLevel: 0 - }); - // Should not contain sensitive information - expect(result).not.toHaveProperty('email'); - expect(result).not.toHaveProperty('password'); - expect(result).not.toHaveProperty('token'); - }); - - it('should map admin user with authLevel 1', () => { - // Arrange - const adminUser = createMockUser({ - id: 'admin-123', - username: 'admin', - email: 'admin@example.com', - fname: 'Admin', - lname: 'User', - state: UserState.ADMIN - }); - - // Act - const result = UserMapper.toShortDto(adminUser); - - // Assert - expect(result).toEqual({ - id: 'admin-123', - username: 'admin', - state: UserState.ADMIN, - authLevel: 1 - }); - }); - }); - - describe('toDetailDto', () => { - it('should map UserAggregate to DetailUserDto correctly', () => { - // Arrange - const user = createMockUser({ - id: 'user-123', - orgid: 'org-456', - username: 'testuser', - email: 'test@example.com', - fname: 'John', - lname: 'Doe', - token: 'verification-token', - type: 'admin', - phone: '+1234567890', - state: UserState.ADMIN - }); - - // Act - const result = UserMapper.toDetailDto(user); - - // Assert - expect(result).toEqual({ - id: 'user-123', - orgid: 'org-456', - username: 'testuser', - email: 'test@example.com', - fname: 'John', - lname: 'Doe', - code: 'verification-token', - type: 'admin', - phone: '+1234567890', - state: UserState.ADMIN - }); - // Should not contain password - expect(result).not.toHaveProperty('password'); - }); - - it('should handle null values correctly', () => { - // Arrange - const user = createMockUser({ - id: 'user-123', - orgid: null, - username: 'testuser', - email: 'test@example.com', - fname: 'John', - lname: 'Doe', - token: null, - type: 'regular', - phone: null, - state: UserState.VERIFIED_REGULAR - }); - - // Act - const result = UserMapper.toDetailDto(user); - - // Assert - expect(result.orgid).toBeNull(); - expect(result.code).toBeNull(); - expect(result.phone).toBeNull(); - }); - }); - - describe('toShortDtoList', () => { - it('should map array of UserAggregate to ShortUserDto array', () => { - // Arrange - const users = [ - createMockUser({ id: 'user-1', username: 'user1', state: UserState.VERIFIED_REGULAR }), - createMockUser({ id: 'user-2', username: 'user2', state: UserState.REGISTERED_NOT_VERIFIED }), - createMockUser({ id: 'user-3', username: 'user3', state: UserState.DEACTIVATED }) - ]; - - // Act - const result = UserMapper.toShortDtoList(users); - - // Assert - expect(result).toHaveLength(3); - expect(result[0]).toEqual({ - id: 'user-1', - username: 'user1', - state: UserState.VERIFIED_REGULAR, - authLevel: 0 - }); - expect(result[1]).toEqual({ - id: 'user-2', - username: 'user2', - state: UserState.REGISTERED_NOT_VERIFIED, - authLevel: 0 - }); - expect(result[2]).toEqual({ - id: 'user-3', - username: 'user3', - state: UserState.DEACTIVATED, - authLevel: 0 - }); - }); - - it('should handle empty array', () => { - // Arrange - const users: UserAggregate[] = []; - - // Act - const result = UserMapper.toShortDtoList(users); - - // Assert - expect(result).toEqual([]); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Deck/commands/CreateDeckCommandHandler.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/Deck/commands/CreateDeckCommandHandler.comprehensive.test.ts deleted file mode 100644 index de0111b7..00000000 --- a/SerpentRace_Backend/tests/Application/Deck/commands/CreateDeckCommandHandler.comprehensive.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { CreateDeckCommandHandler } from '../../../../src/Application/Deck/commands/CreateDeckCommandHandler'; -import { CreateDeckCommand } from '../../../../src/Application/Deck/commands/CreateDeckCommand'; -import { IDeckRepository } from '../../../../src/Domain/IRepository/IDeckRepository'; -import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository'; -import { UserState } from '../../../../src/Domain/User/UserAggregate'; -import { Type as DeckType } from '../../../../src/Domain/Deck/DeckAggregate'; -import { createMockDeck, createMockDeckRepository, createMockUserRepository, createMockOrganizationRepository, createMockUser } from '../../../testUtils'; - -describe('CreateDeckCommandHandler', () => { - let handler: CreateDeckCommandHandler; - let mockDeckRepository: jest.Mocked; - let mockUserRepository: jest.Mocked; - let mockOrganizationRepository: jest.Mocked; - - beforeEach(() => { - jest.clearAllMocks(); - - mockDeckRepository = createMockDeckRepository(); - mockUserRepository = createMockUserRepository(); - mockOrganizationRepository = createMockOrganizationRepository(); - - handler = new CreateDeckCommandHandler(mockDeckRepository, mockUserRepository, mockOrganizationRepository); - }); - - describe('execute', () => { - it('should successfully create a new deck with valid user', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [{ id: 'card-1', name: 'Test Card' }], - }; - - const mockUser = createMockUser({ - id: command.userid, - state: UserState.VERIFIED_REGULAR, - type: 'user' - }); - - const mockDeck = createMockDeck({ - name: command.name, - type: command.type, - userid: command.userid - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(mockUserRepository.findById).toHaveBeenCalledWith(command.userid); - expect(mockDeckRepository.create).toHaveBeenCalled(); - }); - - it('should throw error when user not found', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'nonexistent-user', - cards: [], - }; - - mockUserRepository.findById.mockResolvedValue(null); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('User not found'); - expect(mockUserRepository.findById).toHaveBeenCalledWith(command.userid); - expect(mockDeckRepository.create).not.toHaveBeenCalled(); - }); - - it('should handle admin user creating unlimited decks', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Admin Deck', - type: DeckType.JOKER, - userid: 'admin-123', - cards: [], - }; - - const mockAdminUser = createMockUser({ - id: command.userid, - state: UserState.VERIFIED_REGULAR, - type: 'admin' - }); - - const mockDeck = createMockDeck({ - name: command.name, - type: command.type, - userid: command.userid - }); - - mockUserRepository.findById.mockResolvedValue(mockAdminUser); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(mockDeckRepository.countActiveByUserId).toHaveBeenCalled(); // Admin still checks but bypasses limits - }); - - it('should handle repository creation errors', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [], - }; - - const mockUser = createMockUser({ id: command.userid }); - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockRejectedValue(new Error('Database error')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Database error'); - }); - - it('should create deck with different types', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Question Deck', - type: DeckType.QUESTION, - userid: 'user-123', - cards: [], - }; - - const mockUser = createMockUser({ id: command.userid }); - const mockDeck = createMockDeck({ - name: command.name, - type: command.type, - userid: command.userid - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(mockDeckRepository.create).toHaveBeenCalledWith(expect.objectContaining({ - type: DeckType.QUESTION - })); - }); - - it('should handle empty cards array', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Empty Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [], - }; - - const mockUser = createMockUser({ id: command.userid }); - const mockDeck = createMockDeck(command); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - }); - - it('should check deck limits for regular users', async () => { - // Arrange - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [], - }; - - const mockUser = createMockUser({ - id: command.userid, - type: 'user' - }); - const mockDeck = createMockDeck({ userid: command.userid }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(mockDeck); - - // Act - await handler.execute(command); - - // Assert - expect(mockDeckRepository.countActiveByUserId).toHaveBeenCalledWith(command.userid); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Deck/commands/DeckCommandHandlers.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/Deck/commands/DeckCommandHandlers.comprehensive.test.ts deleted file mode 100644 index 31dd1249..00000000 --- a/SerpentRace_Backend/tests/Application/Deck/commands/DeckCommandHandlers.comprehensive.test.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { CreateDeckCommandHandler } from '../../../../src/Application/Deck/commands/CreateDeckCommandHandler'; -import { UpdateDeckCommandHandler } from '../../../../src/Application/Deck/commands/UpdateDeckCommandHandler'; -import { DeleteDeckCommandHandler } from '../../../../src/Application/Deck/commands/DeleteDeckCommandHandler'; -import { CreateDeckCommand } from '../../../../src/Application/Deck/commands/CreateDeckCommand'; -import { UpdateDeckCommand } from '../../../../src/Application/Deck/commands/UpdateDeckCommand'; -import { DeleteDeckCommand } from '../../../../src/Application/Deck/commands/DeleteDeckCommand'; -import { DeckAggregate, State as DeckState, Type as DeckType, CType } from '../../../../src/Domain/Deck/DeckAggregate'; -import { UserAggregate, UserState } from '../../../../src/Domain/User/UserAggregate'; -import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository'; -import { IDeckRepository } from '../../../../src/Domain/IRepository/IDeckRepository'; -import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository'; -import { - createMockUser, - createMockDeck, - createMockUserRepository, - createMockDeckRepository, - createMockOrganizationRepository, - createMockDate -} from '../../../testUtils'; - -describe('Deck Command Handlers - Comprehensive Coverage', () => { - let mockUserRepository: jest.Mocked; - let mockDeckRepository: jest.Mocked; - let mockOrganizationRepository: jest.Mocked; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - mockDeckRepository = createMockDeckRepository(); - mockOrganizationRepository = createMockOrganizationRepository(); - jest.clearAllMocks(); - }); - - describe('CreateDeckCommandHandler', () => { - let handler: CreateDeckCommandHandler; - - beforeEach(() => { - handler = new CreateDeckCommandHandler(mockDeckRepository, mockUserRepository, mockOrganizationRepository); - }); - - it('should create a new deck successfully', async () => { - // Arrange - const mockUser = createMockUser({ - id: 'user-123', - state: UserState.VERIFIED_REGULAR - }); - const expectedDeck = createMockDeck({ - id: 'deck-123', - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - ctype: CType.PUBLIC, - state: DeckState.ACTIVE, - cards: [] - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.create.mockResolvedValue(expectedDeck); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeTruthy(); - expect(mockUserRepository.findById).toHaveBeenCalledWith('user-123'); - expect(mockDeckRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123' - }) - ); - }); - - it('should throw error when user not found', async () => { - // Arrange - mockUserRepository.findById.mockResolvedValue(null); - - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'nonexistent-user', - cards: [] - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('User not found'); - expect(mockUserRepository.findById).toHaveBeenCalledWith('nonexistent-user'); - expect(mockDeckRepository.create).not.toHaveBeenCalled(); - }); - - it('should handle admin users bypassing restrictions', async () => { - // Arrange - const adminUser = createMockUser({ - id: 'admin-123', - type: 'admin', - state: UserState.ADMIN - }); - const expectedDeck = createMockDeck({ - name: 'Admin Deck', - userid: 'admin-123' - }); - - mockUserRepository.findById.mockResolvedValue(adminUser); - mockDeckRepository.create.mockResolvedValue(expectedDeck); - // Don't mock countActiveByUserId - admin should bypass this check - - const command: CreateDeckCommand = { - name: 'Admin Deck', - type: DeckType.JOKER, - userid: 'admin-123', - cards: [] - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeTruthy(); - expect(mockDeckRepository.countActiveByUserId).not.toHaveBeenCalled(); - }); - - it('should handle different deck types', async () => { - // Arrange - const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR }); - const expectedDeck = createMockDeck({ - name: 'Question Deck', - type: DeckType.QUESTION, - userid: 'user-123' - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.create.mockResolvedValue(expectedDeck); - mockDeckRepository.countActiveByUserId.mockResolvedValue(2); - - const command: CreateDeckCommand = { - name: 'Question Deck', - type: DeckType.QUESTION, - userid: 'user-123', - cards: [] - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeTruthy(); - expect(mockDeckRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - type: DeckType.QUESTION - }) - ); - }); - - it('should handle repository creation errors', async () => { - // Arrange - const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR }); - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockRejectedValue(new Error('Database connection failed')); - - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Database connection failed'); - expect(mockDeckRepository.create).toHaveBeenCalled(); - }); - - it('should handle deck limit restrictions for regular users', async () => { - // Arrange - const mockUser = createMockUser({ - id: 'user-123', - state: UserState.VERIFIED_REGULAR, - type: 'regular' - }); - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(10); // Assuming limit is 10 - - const command: CreateDeckCommand = { - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }; - - // Act & Assert - This should succeed if the limit allows, or fail if over limit - // The exact behavior depends on the business rules in CreateDeckCommandHandler - try { - await handler.execute(command); - // If it succeeds, verify the deck was created - expect(mockDeckRepository.create).toHaveBeenCalled(); - } catch (error) { - // If it fails, verify it's a limit error - expect((error as Error).message).toContain('limit'); - } - }); - }); - - describe('UpdateDeckCommandHandler', () => { - let handler: UpdateDeckCommandHandler; - - beforeEach(() => { - handler = new UpdateDeckCommandHandler(mockDeckRepository); - }); - - it('should update deck successfully', async () => { - // Arrange - const updatedDeck = createMockDeck({ - id: 'deck-123', - name: 'New Name', - ctype: CType.PUBLIC - }); - - mockDeckRepository.update.mockResolvedValue(updatedDeck); - - const command: UpdateDeckCommand = { - id: 'deck-123', - name: 'New Name' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Should return ShortDeckDto format - expect(result).toEqual({ - id: 'deck-123', - name: 'New Name', - type: updatedDeck.type, - playedNumber: updatedDeck.playedNumber, - ctype: updatedDeck.ctype, - }); - expect(mockDeckRepository.update).toHaveBeenCalledWith('deck-123', expect.objectContaining({ - id: 'deck-123', - name: 'New Name' - })); - }); - - it('should return null when deck not found (repository returns null)', async () => { - // Arrange - mockDeckRepository.update.mockResolvedValue(null); - - const command: UpdateDeckCommand = { - id: 'nonexistent-deck', - name: 'New Name' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeNull(); - expect(mockDeckRepository.update).toHaveBeenCalledWith('nonexistent-deck', expect.objectContaining({ - id: 'nonexistent-deck', - name: 'New Name' - })); - }); - - it('should handle partial updates', async () => { - // Arrange - const updatedDeck = createMockDeck({ - id: 'deck-123', - name: 'Original Name', // Name stays the same - ctype: CType.PRIVATE // Only ctype changes - }); - - mockDeckRepository.update.mockResolvedValue(updatedDeck); - - const command: UpdateDeckCommand = { - id: 'deck-123', - ctype: CType.PRIVATE - // Note: name is not provided, should remain unchanged - }; - - // Act - const result = await handler.execute(command); - - // Assert - Should return ShortDeckDto format - expect(result).toEqual({ - id: 'deck-123', - name: 'Original Name', - type: updatedDeck.type, - playedNumber: updatedDeck.playedNumber, - ctype: CType.PRIVATE, - }); - expect(mockDeckRepository.update).toHaveBeenCalledWith('deck-123', expect.objectContaining({ - id: 'deck-123', - ctype: CType.PRIVATE - })); - }); - - it('should handle repository update errors', async () => { - // Arrange - const existingDeck = createMockDeck({ id: 'deck-123' }); - mockDeckRepository.findById.mockResolvedValue(existingDeck); - mockDeckRepository.update.mockRejectedValue(new Error('Update failed')); - - const command: UpdateDeckCommand = { - id: 'deck-123', - name: 'New Name' - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Update failed'); - expect(mockDeckRepository.update).toHaveBeenCalled(); - }); - }); - - describe('DeleteDeckCommandHandler', () => { - let handler: DeleteDeckCommandHandler; - - beforeEach(() => { - handler = new DeleteDeckCommandHandler(mockDeckRepository); - }); - - it('should delete deck successfully (soft delete)', async () => { - // Arrange - mockDeckRepository.softDelete.mockResolvedValue(null); // Soft delete returns void - - const command: DeleteDeckCommand = { - id: 'deck-123', - soft: true // Specify soft delete - }; - - // Act - const result = await handler.execute(command); - - // Assert - DeleteDeckCommandHandler always returns true - expect(result).toBe(true); - expect(mockDeckRepository.softDelete).toHaveBeenCalledWith('deck-123'); - }); - - it('should delete deck successfully (hard delete)', async () => { - // Arrange - mockDeckRepository.delete.mockResolvedValue(null); // Delete returns void - - const command: DeleteDeckCommand = { - id: 'deck-123', - soft: false // Specify hard delete - }; - - // Act - const result = await handler.execute(command); - - // Assert - DeleteDeckCommandHandler always returns true - expect(result).toBe(true); - expect(mockDeckRepository.delete).toHaveBeenCalledWith('deck-123'); - }); - - it('should default to hard delete when soft flag not specified', async () => { - // Arrange - mockDeckRepository.delete.mockResolvedValue(null); - - const command: DeleteDeckCommand = { - id: 'deck-123' - // Note: soft flag not specified, defaults to undefined which is falsy - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockDeckRepository.delete).toHaveBeenCalledWith('deck-123'); - expect(mockDeckRepository.softDelete).not.toHaveBeenCalled(); - }); - - it('should handle repository deletion errors', async () => { - // Arrange - mockDeckRepository.softDelete.mockRejectedValue(new Error('Deletion failed')); - - const command: DeleteDeckCommand = { - id: 'deck-123', - soft: true - }; - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Deletion failed'); - expect(mockDeckRepository.softDelete).toHaveBeenCalledWith('deck-123'); - }); - }); - - describe('Cross-Command Integration Tests', () => { - let createHandler: CreateDeckCommandHandler; - let updateHandler: UpdateDeckCommandHandler; - let deleteHandler: DeleteDeckCommandHandler; - - beforeEach(() => { - createHandler = new CreateDeckCommandHandler(mockDeckRepository, mockUserRepository, mockOrganizationRepository); - updateHandler = new UpdateDeckCommandHandler(mockDeckRepository); - deleteHandler = new DeleteDeckCommandHandler(mockDeckRepository); - }); - - it('should create deck and then update it', async () => { - // Arrange - Create - const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR }); - const createdDeck = createMockDeck({ - id: 'deck-123', - name: 'Initial Name', - userid: 'user-123' - }); - - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(createdDeck); - - // Arrange - Update - const updatedDeck = createMockDeck({ - id: 'deck-123', - name: 'Updated Name', - userid: 'user-123' - }); - mockDeckRepository.findById.mockResolvedValue(createdDeck); - mockDeckRepository.update.mockResolvedValue(updatedDeck); - - // Act - Create - const createCommand: CreateDeckCommand = { - name: 'Initial Name', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }; - const createResult = await createHandler.execute(createCommand); - - // Act - Update - const updateCommand: UpdateDeckCommand = { - id: 'deck-123', - name: 'Updated Name' - }; - const updateResult = await updateHandler.execute(updateCommand); - - // Assert - expect(createResult).toBeTruthy(); - expect(updateResult?.name).toBe('Updated Name'); - expect(mockDeckRepository.create).toHaveBeenCalled(); - expect(mockDeckRepository.update).toHaveBeenCalled(); - }); - - it('should handle full lifecycle: create, update, delete', async () => { - // This tests the complete lifecycle of a deck - const mockUser = createMockUser({ id: 'user-123', state: UserState.VERIFIED_REGULAR }); - const deck = createMockDeck({ id: 'deck-123', userid: 'user-123' }); - - // Setup all mocks - mockUserRepository.findById.mockResolvedValue(mockUser); - mockDeckRepository.countActiveByUserId.mockResolvedValue(0); - mockDeckRepository.create.mockResolvedValue(deck); - mockDeckRepository.update.mockResolvedValue(deck); - mockDeckRepository.softDelete.mockResolvedValue(null); - - // Execute lifecycle - const createResult = await createHandler.execute({ - name: 'Test Deck', - type: DeckType.JOKER, - userid: 'user-123', - cards: [] - }); - - const updateResult = await updateHandler.execute({ - id: 'deck-123', - name: 'Updated Deck' - }); - - const deleteResult = await deleteHandler.execute({ - id: 'deck-123', - soft: true - }); - - // Assert all operations succeeded - expect(createResult).toBeTruthy(); - expect(updateResult).toBeTruthy(); - expect(deleteResult).toBe(true); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Game/BoardGenerationService.test.ts b/SerpentRace_Backend/tests/Application/Game/BoardGenerationService.test.ts deleted file mode 100644 index c3f10169..00000000 --- a/SerpentRace_Backend/tests/Application/Game/BoardGenerationService.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { BoardGenerationService } from '../../../src/Application/Game/BoardGenerationService'; - -// Mock dependencies -jest.mock('../../../src/Application/Services/LoggingService'); - -describe('BoardGenerationService', () => { - let boardGenerationService: BoardGenerationService; - - beforeEach(() => { - boardGenerationService = new BoardGenerationService(); - }); - - describe('generateBoard', () => { - it('should generate a board with the correct number of special fields', async () => { - const positiveFields = 10; - const negativeFields = 8; - const luckFields = 5; - - const result = await boardGenerationService.generateBoard( - positiveFields, - negativeFields, - luckFields - ); - - expect(result).toBeDefined(); - expect(result.fields).toHaveLength(100); - - // Count special fields - const actualPositive = result.fields.filter(f => f.type === 'positive').length; - const actualNegative = result.fields.filter(f => f.type === 'negative').length; - const actualLuck = result.fields.filter(f => f.type === 'luck').length; - - expect(actualPositive).toBe(positiveFields); - expect(actualNegative).toBe(negativeFields); - expect(actualLuck).toBe(luckFields); - }); - - it('should ensure positive fields have positive step values', async () => { - const result = await boardGenerationService.generateBoard(5, 5, 2); - - const positiveFields = result.fields.filter(f => f.type === 'positive'); - positiveFields.forEach(field => { - expect(field.stepValue).toBeGreaterThan(0); - }); - }); - - it('should ensure negative fields have negative step values', async () => { - const result = await boardGenerationService.generateBoard(5, 5, 2); - - const negativeFields = result.fields.filter(f => f.type === 'negative'); - negativeFields.forEach(field => { - expect(field.stepValue).toBeLessThan(0); - }); - }); - - it('should ensure luck fields do not have step values', async () => { - const result = await boardGenerationService.generateBoard(5, 5, 2); - - const luckFields = result.fields.filter(f => f.type === 'luck'); - luckFields.forEach(field => { - expect(field.stepValue).toBeUndefined(); - }); - }); - - it('should produce validation results without -1 values', async () => { - const result = await boardGenerationService.generateBoard(10, 8, 5); - - // Check validation results for invalid moves (-1 values) - let invalidMoves = 0; - let totalMoves = 0; - - Object.values(result.validationResults).forEach(diceOutcomes => { - diceOutcomes.forEach(outcome => { - totalMoves++; - if (outcome === -1) { - invalidMoves++; - } - }); - }); - - const errorRate = totalMoves > 0 ? (invalidMoves / totalMoves) * 100 : 0; - - // Log the results for analysis - console.log(`Error rate: ${errorRate}%`); - console.log(`Invalid moves: ${invalidMoves}/${totalMoves}`); - - // The new algorithm should produce much fewer invalid moves - expect(errorRate).toBeLessThan(50); // Allow some errors but much better than before - }); - - it('should respect the 20-30 movement rule in validation', async () => { - const result = await boardGenerationService.generateBoard(10, 8, 5); - - // Check each validation result to ensure it respects distance rules - Object.entries(result.validationResults).forEach(([fieldPosition, diceOutcomes]) => { - const currentPos = parseInt(fieldPosition); - - diceOutcomes.forEach((outcome, diceIndex) => { - if (outcome !== -1) { // Only check valid moves - const distance = Math.abs(outcome - currentPos); - - if (currentPos <= 85) { - // Fields 1-85: max 20 in any direction - expect(distance).toBeLessThanOrEqual(20); - } else { - // Fields 86-100: max 30 backward, max 20 forward - if (outcome > currentPos) { - expect(distance).toBeLessThanOrEqual(20); // forward - } else { - expect(distance).toBeLessThanOrEqual(30); // backward - } - } - } - }); - }); - }); - - it('should position special fields safely within the safe range', async () => { - const result = await boardGenerationService.generateBoard(10, 8, 5); - - const specialFields = result.fields.filter(f => f.type !== 'regular'); - - // Most special fields should be in the safe range (11-90) for the new algorithm - const safeFields = specialFields.filter(f => f.position >= 11 && f.position <= 90); - const safePercentage = (safeFields.length / specialFields.length) * 100; - - console.log(`Safe field percentage: ${safePercentage}%`); - - // Expect most fields to be positioned safely - expect(safePercentage).toBeGreaterThan(70); - }); - }); -}); \ No newline at end of file diff --git a/SerpentRace_Backend/tests/Application/Organization/commands/OrganizationCommandHandlers.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/Organization/commands/OrganizationCommandHandlers.comprehensive.test.ts deleted file mode 100644 index 80deda87..00000000 --- a/SerpentRace_Backend/tests/Application/Organization/commands/OrganizationCommandHandlers.comprehensive.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { CreateOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/CreateOrganizationCommandHandler'; -import { UpdateOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/UpdateOrganizationCommandHandler'; -import { DeleteOrganizationCommandHandler } from '../../../../src/Application/Organization/commands/DeleteOrganizationCommandHandler'; -import { CreateOrganizationCommand } from '../../../../src/Application/Organization/commands/CreateOrganizationCommand'; -import { UpdateOrganizationCommand } from '../../../../src/Application/Organization/commands/UpdateOrganizationCommand'; -import { DeleteOrganizationCommand } from '../../../../src/Application/Organization/commands/DeleteOrganizationCommand'; -import { OrganizationState } from '../../../../src/Domain/Organization/OrganizationAggregate'; -import { createMockOrganizationRepository, createMockOrganization } from '../../../testUtils'; - -describe('Organization Command Handlers - Comprehensive', () => { - let mockOrganizationRepository: ReturnType; - - beforeEach(() => { - mockOrganizationRepository = createMockOrganizationRepository(); - }); - - describe('CreateOrganizationCommandHandler', () => { - let handler: CreateOrganizationCommandHandler; - - beforeEach(() => { - handler = new CreateOrganizationCommandHandler(mockOrganizationRepository); - }); - - it('should create organization successfully', async () => { - // Arrange - const mockOrgData = createMockOrganization({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactphone: '+1234567890', - contactemail: 'john@testorg.com', - url: null, - state: OrganizationState.REGISTERED - }); - - mockOrganizationRepository.create.mockResolvedValue(mockOrgData); - - const command: CreateOrganizationCommand = { - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@testorg.com', - contactphone: '+1234567890' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Returns ShortOrganizationDto - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Test Organization', - state: 0, - userinorg: 0, - maxOrganizationalDecks: 10 - }); - expect(mockOrganizationRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@testorg.com', - contactphone: '+1234567890', - state: OrganizationState.REGISTERED - }) - ); - }); - - it('should create organization with optional URL', async () => { - // Arrange - const mockOrgData = createMockOrganization({ - id: '550e8400-e29b-41d4-a716-446655440001', - name: 'Org with URL', - contactfname: 'Jane', - contactlname: 'Smith', - contactphone: '+1987654321', - contactemail: 'jane@orgwithurl.com', - url: 'https://orgwithurl.com', - state: OrganizationState.REGISTERED - }); - - mockOrganizationRepository.create.mockResolvedValue(mockOrgData); - - const command: CreateOrganizationCommand = { - name: 'Org with URL', - contactfname: 'Jane', - contactlname: 'Smith', - contactemail: 'jane@orgwithurl.com', - contactphone: '+1987654321', - url: 'https://orgwithurl.com' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440001', - name: 'Org with URL', - state: 0, - userinorg: 0, - maxOrganizationalDecks: 10 - }); - }); - - it('should handle duplicate organization name error', async () => { - // Arrange - const command: CreateOrganizationCommand = { - name: 'Duplicate Org', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@duplicate.com', - contactphone: '+1234567890' - }; - - const duplicateError = new Error('duplicate key value violates unique constraint "organization_name_unique"'); - mockOrganizationRepository.create.mockRejectedValue(duplicateError); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Organization with this name or contact email already exists'); - }); - - it('should handle generic database errors', async () => { - // Arrange - const command: CreateOrganizationCommand = { - name: 'Error Org', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@error.com', - contactphone: '+1234567890' - }; - - mockOrganizationRepository.create.mockRejectedValue(new Error('Database connection failed')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create organization'); - }); - - it('should handle non-Error exceptions', async () => { - // Arrange - const command: CreateOrganizationCommand = { - name: 'Non-Error Exception Org', - contactfname: 'John', - contactlname: 'Doe', - contactemail: 'john@exception.com', - contactphone: '+1234567890' - }; - - mockOrganizationRepository.create.mockRejectedValue('String error'); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create organization'); - }); - }); - - describe('UpdateOrganizationCommandHandler', () => { - let handler: UpdateOrganizationCommandHandler; - - beforeEach(() => { - handler = new UpdateOrganizationCommandHandler(mockOrganizationRepository); - }); - - it('should update organization successfully', async () => { - // Arrange - const updatedOrgData = createMockOrganization({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Updated Organization', - contactemail: 'john@updated.com', - url: 'https://updated.com', - state: OrganizationState.ACTIVE - }); - - mockOrganizationRepository.update.mockResolvedValue(updatedOrgData); - - const command: UpdateOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Updated Organization', - contactemail: 'john@updated.com', - url: 'https://updated.com' - }; - - // Act - const result = await handler.execute(command); - - // Assert - Returns ShortOrganizationDto - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Updated Organization', - state: 1, - userinorg: 0, - maxOrganizationalDecks: 10 - }); - expect(mockOrganizationRepository.update).toHaveBeenCalledWith( - '550e8400-e29b-41d4-a716-446655440000', - command - ); - }); - - it('should return null when organization not found', async () => { - // Arrange - mockOrganizationRepository.update.mockResolvedValue(null); - - const command: UpdateOrganizationCommand = { - id: 'non-existent-id', - name: 'Non-existent Organization' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeNull(); - expect(mockOrganizationRepository.update).toHaveBeenCalledWith('non-existent-id', command); - }); - - it('should update organization with partial data', async () => { - // Arrange - const partialUpdatedOrgData = createMockOrganization({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Original Name', - contactemail: 'john@newmail.com', - state: OrganizationState.ACTIVE - }); - - mockOrganizationRepository.update.mockResolvedValue(partialUpdatedOrgData); - - const command: UpdateOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - contactemail: 'john@newmail.com' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toEqual({ - id: '550e8400-e29b-41d4-a716-446655440000', - name: 'Original Name', - state: 1, - userinorg: 0, - maxOrganizationalDecks: 10 - }); - }); - }); - - describe('DeleteOrganizationCommandHandler', () => { - let handler: DeleteOrganizationCommandHandler; - - beforeEach(() => { - handler = new DeleteOrganizationCommandHandler(mockOrganizationRepository); - }); - - it('should perform soft delete successfully', async () => { - // Arrange - mockOrganizationRepository.softDelete.mockResolvedValue(null); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - soft: true - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockOrganizationRepository.softDelete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockOrganizationRepository.delete).not.toHaveBeenCalled(); - }); - - it('should perform hard delete successfully', async () => { - // Arrange - mockOrganizationRepository.delete.mockResolvedValue(true); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - soft: false - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockOrganizationRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockOrganizationRepository.softDelete).not.toHaveBeenCalled(); - }); - - it('should default to hard delete when soft flag not specified', async () => { - // Arrange - mockOrganizationRepository.delete.mockResolvedValue(true); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000' - }; - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockOrganizationRepository.delete).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000'); - expect(mockOrganizationRepository.softDelete).not.toHaveBeenCalled(); - }); - - it('should handle soft delete with repository error gracefully', async () => { - // Arrange - mockOrganizationRepository.softDelete.mockRejectedValue(new Error('Database error')); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - soft: true - }; - - // Act & Assert - Handler doesn't catch errors, they bubble up - await expect(handler.execute(command)).rejects.toThrow('Database error'); - }); - - it('should handle hard delete with repository error gracefully', async () => { - // Arrange - mockOrganizationRepository.delete.mockRejectedValue(new Error('Database error')); - - const command: DeleteOrganizationCommand = { - id: '550e8400-e29b-41d4-a716-446655440000', - soft: false - }; - - // Act & Assert - Handler doesn't catch errors, they bubble up - await expect(handler.execute(command)).rejects.toThrow('Database error'); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/AuthMiddleware.test.ts b/SerpentRace_Backend/tests/Application/Services/AuthMiddleware.test.ts deleted file mode 100644 index ddac0cac..00000000 --- a/SerpentRace_Backend/tests/Application/Services/AuthMiddleware.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; - -// Mock JWTService before importing anything else -const mockJWTService = { - verify: jest.fn(), - refreshIfNeeded: jest.fn(), - create: jest.fn(), - shouldRefreshToken: jest.fn(), - test: jest.fn(), -}; - -jest.mock('../../../src/Application/Services/JWTService', () => { - return { - JWTService: jest.fn().mockImplementation(() => mockJWTService) - }; -}); - -// Now import the middleware which will use the mocked JWTService -import { authRequired, adminRequired } from '../../../src/Application/Services/AuthMiddleware'; - -describe('AuthMiddleware', () => { - let mockRequest: Partial; - let mockResponse: Partial; - let mockNext: NextFunction; - - beforeEach(() => { - jest.clearAllMocks(); - - mockRequest = { - cookies: {} - }; - - mockResponse = { - status: jest.fn().mockReturnThis(), - json: jest.fn().mockReturnThis(), - cookie: jest.fn() - }; - - mockNext = jest.fn(); - }); - - describe('authRequired', () => { - it('should call next() when token is valid', () => { - // Arrange - const validPayload = { - userId: 'user-123', - authLevel: 0 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(validPayload); - mockJWTService.refreshIfNeeded.mockReturnValue(false); // Token doesn't need refresh - - // Act - authRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(validPayload, mockResponse); - expect((mockRequest as any).user).toBe(validPayload); - expect(mockNext).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - - it('should return 401 when token is invalid', () => { - // Arrange - mockJWTService.verify.mockReturnValue(null); - - // Act - authRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).not.toHaveBeenCalled(); - expect(mockNext).not.toHaveBeenCalled(); - expect(mockResponse.status).toHaveBeenCalledWith(401); - expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Unauthorized' }); - }); - - it('should refresh token when needed', () => { - // Arrange - const validPayload = { - userId: 'user-123', - authLevel: 0 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(validPayload); - mockJWTService.refreshIfNeeded.mockReturnValue(true); // Token needs refresh - - // Act - authRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(validPayload, mockResponse); - expect((mockRequest as any).user).toBe(validPayload); - expect(mockNext).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - }); - - describe('adminRequired', () => { - it('should call next() when token is valid and user is admin', () => { - // Arrange - const adminPayload = { - userId: 'admin-123', - authLevel: 1 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(adminPayload); - mockJWTService.refreshIfNeeded.mockReturnValue(false); - - // Act - adminRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(adminPayload, mockResponse); - expect((mockRequest as any).user).toBe(adminPayload); - expect(mockNext).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - - it('should return 403 when token is invalid', () => { - // Arrange - mockJWTService.verify.mockReturnValue(null); - - // Act - adminRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).not.toHaveBeenCalled(); - expect(mockNext).not.toHaveBeenCalled(); - expect(mockResponse.status).toHaveBeenCalledWith(403); - expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Forbidden' }); - }); - - it('should return 403 when user is not admin', () => { - // Arrange - const regularUserPayload = { - userId: 'user-123', - authLevel: 0 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(regularUserPayload); - - // Act - adminRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).not.toHaveBeenCalled(); - expect(mockNext).not.toHaveBeenCalled(); - expect(mockResponse.status).toHaveBeenCalledWith(403); - expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Forbidden' }); - }); - - it('should refresh token for valid admin user', () => { - // Arrange - const adminPayload = { - userId: 'admin-123', - authLevel: 1 as 0 | 1, - orgId: 'org-123' - }; - - mockJWTService.verify.mockReturnValue(adminPayload); - mockJWTService.refreshIfNeeded.mockReturnValue(true); - - // Act - adminRequired(mockRequest as Request, mockResponse as Response, mockNext); - - // Assert - expect(mockJWTService.verify).toHaveBeenCalledWith(mockRequest); - expect(mockJWTService.refreshIfNeeded).toHaveBeenCalledWith(adminPayload, mockResponse); - expect((mockRequest as any).user).toBe(adminPayload); - expect(mockNext).toHaveBeenCalled(); - expect(mockResponse.status).not.toHaveBeenCalled(); - expect(mockResponse.json).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/ChatConfiguration.test.ts b/SerpentRace_Backend/tests/Application/Services/ChatConfiguration.test.ts deleted file mode 100644 index acde9202..00000000 --- a/SerpentRace_Backend/tests/Application/Services/ChatConfiguration.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { WebSocketService } from '../../../src/Application/Services/WebSocketService'; -import { Server as HttpServer } from 'http'; -import { EventEmitter } from 'events'; - -describe('Chat Configuration', () => { - let mockHttpServer: HttpServer; - - beforeAll(() => { - // Create a more complete HTTP server mock that extends EventEmitter - const httpServerMock = new EventEmitter(); - - // Add necessary methods that Socket.IO expects - Object.assign(httpServerMock, { - on: jest.fn(), - listen: jest.fn(), - close: jest.fn(), - listeners: jest.fn().mockReturnValue([]), - removeListener: jest.fn(), - removeAllListeners: jest.fn(), - setMaxListeners: jest.fn(), - getMaxListeners: jest.fn().mockReturnValue(0), - listenerCount: jest.fn().mockReturnValue(0), - prependListener: jest.fn(), - prependOnceListener: jest.fn(), - off: jest.fn(), - once: jest.fn(), - emit: jest.fn(), - // HTTP server specific - timeout: 0, - keepAliveTimeout: 5000, - maxHeadersCount: null, - headersTimeout: 60000, - requestTimeout: 0 - }); - - mockHttpServer = httpServerMock as unknown as HttpServer; - }); - - afterEach(() => { - // Clean up environment variables - delete process.env.CHAT_MAX_MESSAGES_PER_USER; - delete process.env.CHAT_MESSAGE_CLEANUP_WEEKS; - delete process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES; - }); - - describe('Environment Variable Configuration', () => { - it('should use default chat configuration values', () => { - const service = new WebSocketService(mockHttpServer); - - expect(service['maxMessagesPerUser']).toBe(100); - expect(service['messageCleanupWeeks']).toBe(4); - expect(service['chatTimeout']).toBe(30); - }); - - it('should use environment variable for CHAT_MAX_MESSAGES_PER_USER', () => { - process.env.CHAT_MAX_MESSAGES_PER_USER = '50'; - - const service = new WebSocketService(mockHttpServer); - - expect(service['maxMessagesPerUser']).toBe(50); - }); - - it('should use environment variable for CHAT_MESSAGE_CLEANUP_WEEKS', () => { - // Arrange - process.env.CHAT_MESSAGE_CLEANUP_WEEKS = '8'; - - // Act - const service = new WebSocketService(mockHttpServer); - - // Assert - expect(service['messageCleanupWeeks']).toBe(8); - }); - - it('should use environment variable for CHAT_INACTIVITY_TIMEOUT_MINUTES', () => { - // Arrange - process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES = '60'; - - // Act - const service = new WebSocketService(mockHttpServer); - - // Assert - expect(service['chatTimeout']).toBe(60); - }); - - it('should handle invalid numeric environment variables gracefully', () => { - // Arrange - process.env.CHAT_MAX_MESSAGES_PER_USER = 'invalid'; - process.env.CHAT_MESSAGE_CLEANUP_WEEKS = 'also-invalid'; - process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES = 'not-a-number'; - - // Act - const service = new WebSocketService(mockHttpServer); - - // Assert - parseInt of invalid strings returns NaN - expect(service['maxMessagesPerUser']).toBe(NaN); - expect(service['messageCleanupWeeks']).toBe(NaN); - expect(service['chatTimeout']).toBe(NaN); - }); - }); - - describe('Rate Limiting Logic', () => { - it('should initialize with empty user message counts', () => { - // Act - const service = new WebSocketService(mockHttpServer); - - // Assert - expect(service['userMessageCounts']).toBeDefined(); - expect(service['userMessageCounts'].size).toBe(0); - }); - - it('should allow messages within rate limit', () => { - // Arrange - process.env.CHAT_MAX_MESSAGES_PER_USER = '5'; - const service = new WebSocketService(mockHttpServer); - const userId = 'test-user'; - - // Act & Assert - should allow first 5 messages - for (let i = 0; i < 5; i++) { - expect(service['checkMessageRateLimit'](userId)).toBe(true); - } - }); - - it('should block messages when rate limit exceeded', () => { - // Arrange - process.env.CHAT_MAX_MESSAGES_PER_USER = '3'; - const service = new WebSocketService(mockHttpServer); - const userId = 'test-user'; - - // Act - send 3 messages (should be allowed) - for (let i = 0; i < 3; i++) { - expect(service['checkMessageRateLimit'](userId)).toBe(true); - } - - // Assert - 4th message should be blocked - expect(service['checkMessageRateLimit'](userId)).toBe(false); - }); - - it('should reset rate limit after time window', (done) => { - // Arrange - process.env.CHAT_MAX_MESSAGES_PER_USER = '2'; - const service = new WebSocketService(mockHttpServer); - const userId = 'test-user'; - - // Act - exhaust rate limit - expect(service['checkMessageRateLimit'](userId)).toBe(true); - expect(service['checkMessageRateLimit'](userId)).toBe(true); - expect(service['checkMessageRateLimit'](userId)).toBe(false); // Should be blocked - - // Mock time passage by manipulating the internal state - const userStats = service['userMessageCounts'].get(userId)!; - userStats.lastReset = Date.now() - (60 * 1000 + 1); // More than 1 minute ago - service['userMessageCounts'].set(userId, userStats); - - // Assert - should be allowed again after reset - expect(service['checkMessageRateLimit'](userId)).toBe(true); - done(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/DIContainer.test.ts b/SerpentRace_Backend/tests/Application/Services/DIContainer.test.ts deleted file mode 100644 index 0b174b32..00000000 --- a/SerpentRace_Backend/tests/Application/Services/DIContainer.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { container } from '../../../src/Application/Services/DIContainer'; -import { IUserRepository } from '../../../src/Domain/IRepository/IUserRepository'; -import { IChatRepository } from '../../../src/Domain/IRepository/IChatRepository'; -import { LoggingService } from '../../../src/Application/Services/LoggingService'; - -describe('DIContainer', () => { - // Cleanup after all tests to prevent Jest hanging - afterAll(async () => { - await LoggingService.getInstance().shutdown(); - }); - - describe('Repositories', () => { - it('should return singleton IUserRepository instance', () => { - const repo1 = container.userRepository; - const repo2 = container.userRepository; - - expect(repo1).toBeTruthy(); - expect(repo1).toBe(repo2); // Same instance (singleton) - expect(typeof repo1.findById).toBe('function'); // Has interface methods - }); - - it('should return singleton IChatRepository instance', () => { - const repo1 = container.chatRepository; - const repo2 = container.chatRepository; - - expect(repo1).toBeTruthy(); - expect(repo1).toBe(repo2); // Same instance (singleton) - expect(typeof repo1.findById).toBe('function'); // Has interface methods - }); - }); - - describe('Command Handlers', () => { - it('should return singleton CreateUserCommandHandler instance', () => { - const handler1 = container.createUserCommandHandler; - const handler2 = container.createUserCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton LoginCommandHandler instance', () => { - const handler1 = container.loginCommandHandler; - const handler2 = container.loginCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton DeactivateUserCommandHandler instance', () => { - const handler1 = container.deactivateUserCommandHandler; - const handler2 = container.deactivateUserCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton DeleteUserCommandHandler instance', () => { - const handler1 = container.deleteUserCommandHandler; - const handler2 = container.deleteUserCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton DeleteDeckCommandHandler instance', () => { - const handler1 = container.deleteDeckCommandHandler; - const handler2 = container.deleteDeckCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton DeleteOrganizationCommandHandler instance', () => { - const handler1 = container.deleteOrganizationCommandHandler; - const handler2 = container.deleteOrganizationCommandHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - }); - - describe('Query Handlers', () => { - it('should return singleton GetUserByIdQueryHandler instance', () => { - const handler1 = container.getUserByIdQueryHandler; - const handler2 = container.getUserByIdQueryHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - - it('should return singleton GetUsersByPageQueryHandler instance', () => { - const handler1 = container.getUsersByPageQueryHandler; - const handler2 = container.getUsersByPageQueryHandler; - - expect(handler1).toBeTruthy(); - expect(handler1).toBe(handler2); // Same instance (singleton) - }); - }); - - describe('Services', () => { - it('should return singleton JWTService instance', () => { - const service1 = container.jwtService; - const service2 = container.jwtService; - - expect(service1).toBeTruthy(); - expect(service1).toBe(service2); // Same instance (singleton) - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/EmailService.test.ts b/SerpentRace_Backend/tests/Application/Services/EmailService.test.ts deleted file mode 100644 index d0b1ab3a..00000000 --- a/SerpentRace_Backend/tests/Application/Services/EmailService.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { EmailService, EmailOptions } from '../../../src/Application/Services/EmailService'; -import * as nodemailer from 'nodemailer'; -import * as fs from 'fs'; - -// Mock nodemailer -jest.mock('nodemailer'); -jest.mock('fs'); - -// Mock logger -jest.mock('../../../src/Application/Services/Logger', () => ({ - logError: jest.fn(), - logAuth: jest.fn(), - logStartup: jest.fn(), -})); - -describe('EmailService', () => { - let emailService: EmailService; - let mockTransporter: jest.Mocked; - let mockCreateTransporter: jest.MockedFunction; - - beforeEach(() => { - jest.clearAllMocks(); - - // Mock nodemailer.createTransporter - mockTransporter = { - sendMail: jest.fn(), - } as any; - - mockCreateTransporter = nodemailer.createTransport as jest.MockedFunction; - mockCreateTransporter.mockReturnValue(mockTransporter); - - // Mock fs - (fs.readFileSync as jest.Mock).mockImplementation((filePath: string) => { - if (filePath.includes('html')) { - return 'HTML template: {{name}}'; - } - return 'Text template: {{name}}'; - }); - - (fs.existsSync as jest.Mock).mockReturnValue(true); - - emailService = new EmailService(); - }); - - describe('sendEmail', () => { - it('should send email successfully', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - html: '

Test HTML

', - text: 'Test Text', - }; - - mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(true); - expect(mockTransporter.sendMail).toHaveBeenCalledWith({ - from: process.env.EMAIL_FROM || 'noreply@serpentrace.com', - to: emailOptions.to, - subject: emailOptions.subject, - html: emailOptions.html, - text: emailOptions.text, - attachments: expect.any(Array), - }); - }); - - it('should send email with template', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - template: 'verification', - templateData: { name: 'John', token: 'abc123' }, - }; - - mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(true); - expect(mockTransporter.sendMail).toHaveBeenCalledWith({ - from: process.env.EMAIL_FROM || 'noreply@serpentrace.com', - to: emailOptions.to, - subject: emailOptions.subject, - html: expect.any(String), - text: expect.any(String), - attachments: expect.any(Array), - }); - }); - - it('should handle email send failure', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - text: 'Test Text', - }; - - mockTransporter.sendMail.mockRejectedValue(new Error('SMTP Error')); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(false); - }); - - it('should handle missing template files', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - template: 'nonexistent', - templateData: { name: 'John' }, - }; - - (fs.existsSync as jest.Mock).mockReturnValue(false); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(false); - }); - - it('should handle template processing errors', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - template: 'verification', - templateData: { name: 'John' }, - }; - - (fs.readFileSync as jest.Mock).mockImplementation(() => { - throw new Error('File read error'); - }); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(false); - }); - - it('should use fallback content when template data is missing', async () => { - // Arrange - const emailOptions: EmailOptions = { - to: 'test@example.com', - subject: 'Test Subject', - template: 'verification', - }; - - mockTransporter.sendMail.mockResolvedValue({ messageId: 'test-id' }); - - // Act - const result = await emailService.sendEmail(emailOptions); - - // Assert - expect(result).toBe(true); - }); - }); - - describe('constructor', () => { - it('should initialize with environment variables', () => { - // Arrange - const originalEnv = process.env; - process.env = { - ...originalEnv, - EMAIL_HOST: 'test-smtp.com', - EMAIL_PORT: '465', - EMAIL_SECURE: 'true', - EMAIL_USER: 'test@example.com', - EMAIL_PASS: 'testpass', - EMAIL_FROM: 'sender@example.com', - }; - - // Act - const service = new EmailService(); - - // Assert - expect(mockCreateTransporter).toHaveBeenCalledWith({ - host: 'test-smtp.com', - port: 465, - secure: true, - auth: { - user: 'test@example.com', - pass: 'testpass', - }, - }); - - // Restore environment - process.env = originalEnv; - }); - - it('should use default values when environment variables are missing', () => { - // Arrange - const originalEnv = process.env; - process.env = {}; - - // Act - const service = new EmailService(); - - // Assert - expect(mockCreateTransporter).toHaveBeenCalledWith({ - host: 'smtp.gmail.com', - port: 587, - secure: false, - auth: { - user: '', - pass: '', - }, - }); - - // Restore environment - process.env = originalEnv; - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/JWTService.refresh.test.ts b/SerpentRace_Backend/tests/Application/Services/JWTService.refresh.test.ts deleted file mode 100644 index b679f25c..00000000 --- a/SerpentRace_Backend/tests/Application/Services/JWTService.refresh.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { JWTService, TokenPayload } from '../../../src/Application/Services/JWTService'; -import { Request, Response } from 'express'; -import { UserState } from '../../../src/Domain/User/UserAggregate'; - -describe('JWTService - Token Refresh Logic', () => { - let jwtService: JWTService; - let mockRequest: Partial; - let mockResponse: Partial; - let dateNowSpy: jest.SpyInstance; - - beforeEach(() => { - jwtService = new JWTService(); - - mockRequest = { - cookies: {} - }; - - mockResponse = { - cookie: jest.fn() - }; - - // Create a fresh spy for Date.now in each test - dateNowSpy = jest.spyOn(Date, 'now'); - }); - - afterEach(() => { - // Always restore Date.now after each test - dateNowSpy.mockRestore(); - }); - - describe('shouldRefreshToken', () => { - it('should return true when token is 75% through its lifetime', () => { - // Token issued at time 100, expires at 900 (lifetime: 800) - // 75% of 800 = 600, so at time 700 (100 + 600), it should refresh - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 700 (which is 75% through the token lifetime) - dateNowSpy.mockReturnValue(700 * 1000); - - const result = jwtService.shouldRefreshToken(payload); - expect(result).toBe(true); - }); - - it('should return true when token is more than 75% through its lifetime', () => { - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 750 (which is 81.25% through the token lifetime) - dateNowSpy.mockReturnValue(750 * 1000); - - const result = jwtService.shouldRefreshToken(payload); - expect(result).toBe(true); - }); - - it('should return false when token is less than 75% through its lifetime', () => { - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 600 (which is 62.5% through the token lifetime) - dateNowSpy.mockReturnValue(600 * 1000); - - const result = jwtService.shouldRefreshToken(payload); - expect(result).toBe(false); - }); - - it('should return false when payload does not have required timestamp fields', () => { - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org' - }; - - const result = jwtService.shouldRefreshToken(payload); - expect(result).toBe(false); - }); - }); - - describe('refreshIfNeeded', () => { - it('should return new token when refresh is needed', () => { - // Setup a payload that needs refresh (75% through lifetime) - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 700 (75% through the token lifetime) - dateNowSpy.mockReturnValue(700 * 1000); - - const result = jwtService.refreshIfNeeded(payload, mockResponse as Response); - - expect(result).toBe(true); - expect(mockResponse.cookie).toHaveBeenCalled(); - }); - - it('should return false when refresh is not needed', () => { - // Setup a payload that doesn't need refresh (less than 75% through lifetime) - const payload: TokenPayload = { - userId: 'test-user', - authLevel: 0 as 0 | 1, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'test-org', - iat: 100, - exp: 900 - }; - - // Mock current time as 600 (62.5% through the token lifetime) - dateNowSpy.mockReturnValue(600 * 1000); - - const result = jwtService.refreshIfNeeded(payload, mockResponse as Response); - - expect(result).toBe(false); - expect(mockResponse.cookie).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/JWTService.test.ts b/SerpentRace_Backend/tests/Application/Services/JWTService.test.ts deleted file mode 100644 index 0a003163..00000000 --- a/SerpentRace_Backend/tests/Application/Services/JWTService.test.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { JWTService, TokenPayload } from '../../../src/Application/Services/JWTService'; -import { Request, Response } from 'express'; -import { UserState } from '../../../src/Domain/User/UserAggregate'; - - -describe('JWTService', () => { - let jwtService: JWTService; - let mockRequest: Partial; - let mockResponse: Partial; - - beforeEach(() => { - jest.clearAllMocks(); - jwtService = new JWTService(); - - // Set a test secret for consistent testing - process.env.JWT_SECRET = 'test-secret-key-for-testing'; - process.env.JWT_EXPIRY = '3600'; // 1 hour - - // Mock express Request and Response - mockRequest = { - cookies: {} - }; - - mockResponse = { - cookie: jest.fn() - }; - }); - - afterEach(() => { - // Clean up environment - delete process.env.JWT_SECRET; - delete process.env.JWT_EXPIRY; - }); - - describe('create', () => { - it('should create a valid JWT token and set cookie', () => { - // Arrange - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - // Act - const token = jwtService.create(payload, mockResponse as Response); - - // Assert - expect(token).toBeDefined(); - expect(typeof token).toBe('string'); - expect(token.split('.')).toHaveLength(3); // JWT has 3 parts - expect(mockResponse.cookie).toHaveBeenCalledWith( - 'auth_token', - token, - expect.objectContaining({ - httpOnly: true, - sameSite: 'strict', - maxAge: 86400000 // 24 hours in milliseconds - }) - ); - }); - - it('should create different tokens for different payloads', () => { - // Arrange - const payload1: TokenPayload = { - userId: 'user-1', - authLevel: 0 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-1' - }; - - const payload2: TokenPayload = { - userId: 'user-2', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_PREMIUM, - orgId: 'org-2' - }; - - // Act - const token1 = jwtService.create(payload1, mockResponse as Response); - const token2 = jwtService.create(payload2, mockResponse as Response); - - // Assert - expect(token1).toBeDefined(); - expect(token2).toBeDefined(); - expect(token1).not.toBe(token2); - }); - - it('should set secure cookie in production environment', () => { - // Arrange - process.env.NODE_ENV = 'production'; - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - // Act - const token = jwtService.create(payload, mockResponse as Response); - - // Assert - expect(mockResponse.cookie).toHaveBeenCalledWith( - 'auth_token', - token, - expect.objectContaining({ - secure: true - }) - ); - - // Clean up - delete process.env.NODE_ENV; - }); - }); - - describe('verify', () => { - it('should verify a valid token from cookies', () => { - // Arrange - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = jwtService.create(payload, mockResponse as Response); - mockRequest.cookies = { auth_token: token }; - - // Act - const result = jwtService.verify(mockRequest as Request); - - // Assert - expect(result).toBeDefined(); - expect(result!.userId).toBe('user-123'); - expect(result!.authLevel).toBe(1); - expect(result!.orgId).toBe('org-456'); - }); - - it('should return null when no token is present in cookies', () => { - // Arrange - mockRequest.cookies = {}; - - // Act - const result = jwtService.verify(mockRequest as Request); - - // Assert - expect(result).toBeNull(); - }); - - it('should return null for invalid token', () => { - // Arrange - mockRequest.cookies = { auth_token: 'invalid.jwt.token' }; - - // Act - const result = jwtService.verify(mockRequest as Request); - - // Assert - expect(result).toBeNull(); - }); - - it('should return null for malformed token', () => { - // Arrange - mockRequest.cookies = { auth_token: 'not-a-jwt-token' }; - - // Act - const result = jwtService.verify(mockRequest as Request); - - // Assert - expect(result).toBeNull(); - }); - }); - - describe('token creation with different payloads', () => { - it('should create tokens with dynamic user data', () => { - // Arrange - const timestamp = Date.now(); - const testPayload: TokenPayload = { - userId: `test-user-${timestamp}`, - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: `test-org-${timestamp}` - }; - - // Act - const token = jwtService.create(testPayload, mockResponse as Response); - - // Assert - expect(token).toBeDefined(); - expect(typeof token).toBe('string'); - expect(mockResponse.cookie).toHaveBeenCalled(); - - // Verify we can decode it back - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = jwtService.verify(mockRequest as Request); - - expect(verifiedPayload).toBeDefined(); - expect(verifiedPayload!.userId).toBe(testPayload.userId); - expect(verifiedPayload!.orgId).toBe(testPayload.orgId); - expect(verifiedPayload!.authLevel).toBe(testPayload.authLevel); - }); - - it('should create different tokens for different timestamps', async () => { - // Arrange - const timestamp1 = Date.now(); - const payload1: TokenPayload = { - userId: `test-user-${timestamp1}`, - authLevel: 0 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: `test-org-${timestamp1}` - }; - - // Add a small delay to ensure different timestamps - await new Promise(resolve => setTimeout(resolve, 1)); - - const timestamp2 = Date.now(); - const payload2: TokenPayload = { - userId: `test-user-${timestamp2}`, - authLevel: 1 as const, - userStatus: UserState.VERIFIED_PREMIUM, - orgId: `test-org-${timestamp2}` - }; - - // Act - const token1 = jwtService.create(payload1, mockResponse as Response); - const token2 = jwtService.create(payload2, mockResponse as Response); - - // Assert - expect(token1).not.toBe(token2); - expect(payload1.userId).not.toBe(payload2.userId); - expect(payload1.orgId).not.toBe(payload2.orgId); - }); - }); - - describe('integration scenarios', () => { - it('should create and verify token in complete flow', () => { - // Arrange - const originalPayload: TokenPayload = { - userId: 'integration-user', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'integration-org' - }; - - // Act - Complete flow - const token = jwtService.create(originalPayload, mockResponse as Response); - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = jwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - expect(verifiedPayload!.userId).toBe('integration-user'); - expect(verifiedPayload!.authLevel).toBe(1); - expect(verifiedPayload!.orgId).toBe('integration-org'); - }); - }); - - describe('JWT_EXPIRATION duration parsing', () => { - it('should parse JWT_EXPIRATION in hours format', () => { - // Arrange - delete process.env.JWT_EXPIRY; - process.env.JWT_EXPIRATION = '2h'; - - // Act - const newJwtService = new JWTService(); - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = newJwtService.create(payload, mockResponse as Response); - - // Update mock request with the created token - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = newJwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - expect(verifiedPayload!.exp).toBeDefined(); - - // Token should expire in approximately 2 hours (7200 seconds) - const expectedExp = Math.floor(Date.now() / 1000) + 7200; - expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds - - // Cleanup - delete process.env.JWT_EXPIRATION; - }); - - it('should parse JWT_EXPIRATION in days format', () => { - // Arrange - delete process.env.JWT_EXPIRY; - process.env.JWT_EXPIRATION = '7d'; - - // Act - const newJwtService = new JWTService(); - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = newJwtService.create(payload, mockResponse as Response); - - // Update mock request with the created token - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = newJwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - - // Token should expire in approximately 7 days (604800 seconds) - const expectedExp = Math.floor(Date.now() / 1000) + 604800; - expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds - - // Cleanup - delete process.env.JWT_EXPIRATION; - }); - - it('should parse JWT_EXPIRATION in minutes format', () => { - // Arrange - delete process.env.JWT_EXPIRY; - process.env.JWT_EXPIRATION = '30m'; - - // Act - const newJwtService = new JWTService(); - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = newJwtService.create(payload, mockResponse as Response); - - // Update mock request with the created token - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = newJwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - - // Token should expire in approximately 30 minutes (1800 seconds) - const expectedExp = Math.floor(Date.now() / 1000) + 1800; - expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds - - // Cleanup - delete process.env.JWT_EXPIRATION; - }); - - it('should prioritize JWT_EXPIRY over JWT_EXPIRATION when both are set', () => { - // Arrange - process.env.JWT_EXPIRY = '1800'; // 30 minutes in seconds - process.env.JWT_EXPIRATION = '1h'; // 1 hour - - // Act - const newJwtService = new JWTService(); - const payload: TokenPayload = { - userId: 'user-123', - authLevel: 1 as const, - userStatus: UserState.VERIFIED_REGULAR, - orgId: 'org-456' - }; - - const token = newJwtService.create(payload, mockResponse as Response); - - // Update mock request with the created token - mockRequest.cookies = { auth_token: token }; - const verifiedPayload = newJwtService.verify(mockRequest as Request); - - // Assert - expect(token).toBeDefined(); - expect(verifiedPayload).toBeDefined(); - - // Should use JWT_EXPIRY (1800 seconds), not JWT_EXPIRATION (3600 seconds) - const expectedExp = Math.floor(Date.now() / 1000) + 1800; - expect(verifiedPayload!.exp).toBeCloseTo(expectedExp, -1); // Within 10 seconds - - // Cleanup - delete process.env.JWT_EXPIRY; - delete process.env.JWT_EXPIRATION; - }); - - it('should throw error for invalid JWT_EXPIRATION format', () => { - // Arrange - delete process.env.JWT_EXPIRY; - process.env.JWT_EXPIRATION = 'invalid-format'; - - // Act & Assert - expect(() => { - new JWTService(); - }).toThrow('Invalid duration format: invalid-format. Use format like \'24h\', \'7d\', \'30m\''); - - // Cleanup - delete process.env.JWT_EXPIRATION; - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/LoggingService.test.ts b/SerpentRace_Backend/tests/Application/Services/LoggingService.test.ts deleted file mode 100644 index 9978141d..00000000 --- a/SerpentRace_Backend/tests/Application/Services/LoggingService.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { LoggingService, LogLevel } from '../../../src/Application/Services/LoggingService'; -import { logAuth, logError, logDatabase, logStartup } from '../../../src/Application/Services/Logger'; -import fs from 'fs'; -import path from 'path'; - -describe('LoggingService', () => { - let loggingService: LoggingService; - const testLogsDir = path.join(process.cwd(), 'test-logs'); - - beforeEach(() => { - // Clean up any existing test logs - if (fs.existsSync(testLogsDir)) { - fs.rmSync(testLogsDir, { recursive: true, force: true }); - } - - // Mock environment variables for testing - process.env.MAX_LOGS_PER_FILE = '10'; - process.env.MINIO_ENDPOINT = ''; - - loggingService = LoggingService.getInstance(); - }); - - afterEach(() => { - // Clean up test logs - if (fs.existsSync(testLogsDir)) { - fs.rmSync(testLogsDir, { recursive: true, force: true }); - } - - // Clean up environment variables - delete process.env.MAX_LOGS_PER_FILE; - delete process.env.MINIO_ENDPOINT; - }); - - describe('Log Level Functions', () => { - it('should log authentication events', () => { - const consoleSpy = jest.spyOn(console, 'info').mockImplementation(); - - logAuth('Test auth message', 'user123', { action: 'login' }); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('[AUTH]'); - expect(logCall).toContain('Test auth message'); - - consoleSpy.mockRestore(); - }); - - it('should log error events with stack trace', () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - const testError = new Error('Test error message'); - - logError('Test error occurred', testError); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('[ERROR]'); - expect(logCall).toContain('Test error occurred'); - - consoleSpy.mockRestore(); - }); - - it('should log database operations with timing', () => { - const consoleSpy = jest.spyOn(console, 'info').mockImplementation(); - - logDatabase('Query executed', 'SELECT * FROM users', 45); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('[DATABASE]'); - expect(logCall).toContain('Query executed'); - - consoleSpy.mockRestore(); - }); - - it('should log startup events', () => { - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - - logStartup('Application started', { version: '1.0.0' }); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('[STARTUP]'); - expect(logCall).toContain('Application started'); - - consoleSpy.mockRestore(); - }); - }); - - describe('Log Formatting', () => { - it('should include timestamp in log entries', () => { - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - - logStartup('Test message'); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - - // Check if timestamp is in ISO format - const timestampRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/; - expect(logCall).toMatch(timestampRegex); - - consoleSpy.mockRestore(); - }); - - it('should include metadata in log entries', () => { - const consoleSpy = jest.spyOn(console, 'info').mockImplementation(); - const metadata = { userId: '123', action: 'test' }; - - logAuth('Test with metadata', 'user123', metadata); - - expect(consoleSpy).toHaveBeenCalled(); - const logCall = consoleSpy.mock.calls[0][0]; - expect(logCall).toContain('Meta:'); - expect(logCall).toContain('"userId":"123"'); - expect(logCall).toContain('"action":"test"'); - - consoleSpy.mockRestore(); - }); - }); - - describe('Request Logging Middleware', () => { - it('should create request logging middleware', () => { - const middleware = loggingService.requestLoggingMiddleware(); - - expect(typeof middleware).toBe('function'); - expect(middleware.length).toBe(3); // req, res, next - }); - - it('should create error logging middleware', () => { - const middleware = loggingService.errorLoggingMiddleware(); - - expect(typeof middleware).toBe('function'); - expect(middleware.length).toBe(4); // error, req, res, next - }); - }); - - describe('Log Levels', () => { - it('should have all required log levels defined', () => { - expect(LogLevel.REQUEST).toBe('REQUEST'); - expect(LogLevel.ERROR).toBe('ERROR'); - expect(LogLevel.WARNING).toBe('WARNING'); - expect(LogLevel.AUTH).toBe('AUTH'); - expect(LogLevel.DATABASE).toBe('DATABASE'); - expect(LogLevel.STARTUP).toBe('STARTUP'); - expect(LogLevel.CONNECTION).toBe('CONNECTION'); - expect(LogLevel.OTHER).toBe('OTHER'); - }); - }); - - describe('Singleton Pattern', () => { - it('should return the same instance', () => { - const instance1 = LoggingService.getInstance(); - const instance2 = LoggingService.getInstance(); - - expect(instance1).toBe(instance2); - }); - }); - - describe('File Operations', () => { - it('should handle missing Minio configuration gracefully', () => { - // Test that the service starts without Minio config - expect(() => LoggingService.getInstance()).not.toThrow(); - }); - - it('should generate monthly directory structure', () => { - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - const expectedPath = path.join('logs', `${year}-${month}`); - - // This tests the internal logic through the public interface - logStartup('Test for directory creation'); - - // Since we can't directly test the private method, we verify the service doesn't crash - expect(loggingService).toBeDefined(); - }); - }); - - describe('Error Handling', () => { - it('should handle logging errors gracefully', () => { - // Mock fs.appendFileSync to throw an error - const originalAppendFileSync = fs.appendFileSync; - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - - fs.appendFileSync = jest.fn(() => { - throw new Error('Disk full'); - }); - - expect(() => { - logStartup('This should not crash'); - }).not.toThrow(); - - // Restore original function - fs.appendFileSync = originalAppendFileSync; - consoleSpy.mockRestore(); - }); - - it('should continue logging to console even if file logging fails', () => { - const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - - // Mock file system to fail - const originalAppendFileSync = fs.appendFileSync; - fs.appendFileSync = jest.fn(() => { - throw new Error('File system error'); - }); - - logStartup('Test message'); - - // Should still log to console - expect(consoleSpy).toHaveBeenCalled(); - - // Restore - fs.appendFileSync = originalAppendFileSync; - consoleSpy.mockRestore(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/PasswordService.test.ts b/SerpentRace_Backend/tests/Application/Services/PasswordService.test.ts deleted file mode 100644 index a517c6f2..00000000 --- a/SerpentRace_Backend/tests/Application/Services/PasswordService.test.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { PasswordService } from '../../../src/Application/Services/PasswordService'; - -// Mock bcrypt completely -jest.mock('bcrypt'); - -describe('PasswordService', () => { - // Mock functions for bcrypt - const mockBcryptHash = jest.fn(); - const mockBcryptCompare = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - // Reset console.error mock to avoid noise in tests - jest.spyOn(console, 'error').mockImplementation(() => {}); - - // Setup bcrypt mocks - const bcrypt = require('bcrypt'); - bcrypt.hash = mockBcryptHash; - bcrypt.compare = mockBcryptCompare; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('hashPassword', () => { - it('should hash a valid password successfully', async () => { - // Arrange - const password = 'validPassword123!'; - const hashedPassword = '$2b$12$hashed.password.here'; - - mockBcryptHash.mockResolvedValue(hashedPassword); - - // Act - const result = await PasswordService.hashPassword(password); - - // Assert - expect(result).toBe(hashedPassword); - expect(mockBcryptHash).toHaveBeenCalledWith(password, 12); - }); - - it('should throw error for empty password', async () => { - // Arrange - const password = ''; - - // Act & Assert - await expect(PasswordService.hashPassword(password)).rejects.toThrow('Password must be a non-empty string'); - expect(mockBcryptHash).not.toHaveBeenCalled(); - }); - - it('should throw error for non-string password', async () => { - // Arrange - const password = null as any; - - // Act & Assert - await expect(PasswordService.hashPassword(password)).rejects.toThrow('Password must be a non-empty string'); - expect(mockBcryptHash).not.toHaveBeenCalled(); - }); - - it('should handle bcrypt errors and throw generic error', async () => { - // Arrange - const password = 'validPassword123!'; - mockBcryptHash.mockRejectedValue(new Error('Bcrypt error')); - - // Act & Assert - await expect(PasswordService.hashPassword(password)).rejects.toThrow('Failed to hash password'); - expect(mockBcryptHash).toHaveBeenCalledWith(password, 12); - }); - }); - - describe('verifyPassword', () => { - it('should return true for matching password and hash', async () => { - // Arrange - const password = 'validPassword123!'; - const hashedPassword = '$2b$12$hashed.password.here'; - - mockBcryptCompare.mockResolvedValue(true); - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(true); - expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword); - }); - - it('should return false for non-matching password and hash', async () => { - // Arrange - const password = 'wrongPassword'; - const hashedPassword = '$2b$12$hashed.password.here'; - - mockBcryptCompare.mockResolvedValue(false); - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword); - }); - - it('should return false for empty password', async () => { - // Arrange - const password = ''; - const hashedPassword = '$2b$12$hashed.password.here'; - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).not.toHaveBeenCalled(); - }); - - it('should return false for empty hashed password', async () => { - // Arrange - const password = 'validPassword123!'; - const hashedPassword = ''; - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).not.toHaveBeenCalled(); - }); - - it('should return false for non-string inputs', async () => { - // Arrange - const password = null as any; - const hashedPassword = undefined as any; - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).not.toHaveBeenCalled(); - }); - - it('should return false when bcrypt throws error', async () => { - // Arrange - const password = 'validPassword123!'; - const hashedPassword = '$2b$12$hashed.password.here'; - - mockBcryptCompare.mockRejectedValue(new Error('Bcrypt compare error')); - - // Act - const result = await PasswordService.verifyPassword(password, hashedPassword); - - // Assert - expect(result).toBe(false); - expect(mockBcryptCompare).toHaveBeenCalledWith(password, hashedPassword); - }); - }); - - describe('validatePasswordStrength', () => { - it('should return valid for strong password', () => { - // Arrange - const password = 'StrongPass123!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(true); - expect(result.errors).toEqual([]); - }); - - it('should return invalid for short password', () => { - // Arrange - const password = 'Short1!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must be at least 8 characters long'); - }); - - it('should return invalid for password without uppercase', () => { - // Arrange - const password = 'lowercase123!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must contain at least one uppercase letter'); - }); - - it('should return invalid for password without lowercase', () => { - // Arrange - const password = 'UPPERCASE123!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must contain at least one lowercase letter'); - }); - - it('should return invalid for password without numbers', () => { - // Arrange - const password = 'NoNumbers!'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must contain at least one number'); - }); - - it('should return invalid for password without special characters', () => { - // Arrange - const password = 'NoSpecial123'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must contain at least one special character'); - }); - - it('should return multiple errors for weak password', () => { - // Arrange - const password = 'weak'; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toHaveLength(4); - expect(result.errors).toContain('Password must be at least 8 characters long'); - expect(result.errors).toContain('Password must contain at least one uppercase letter'); - expect(result.errors).toContain('Password must contain at least one number'); - expect(result.errors).toContain('Password must contain at least one special character'); - }); - - it('should handle empty password', () => { - // Arrange - const password = ''; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must be provided as a string'); - }); - - it('should handle null password', () => { - // Arrange - const password = null as any; - - // Act - const result = PasswordService.validatePasswordStrength(password); - - // Assert - expect(result.isValid).toBe(false); - expect(result.errors).toContain('Password must be provided as a string'); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/RedisService.test.ts b/SerpentRace_Backend/tests/Application/Services/RedisService.test.ts deleted file mode 100644 index 4b558575..00000000 --- a/SerpentRace_Backend/tests/Application/Services/RedisService.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { RedisService } from '../../../src/Application/Services/RedisService'; -import { logStartup, logError } from '../../../src/Application/Services/Logger'; - -describe('RedisService', () => { - let redisService: RedisService; - - beforeAll(async () => { - redisService = RedisService.getInstance(); - - try { - await redisService.connect(); - } catch (error) { - console.log('Redis not available for testing, skipping Redis tests'); - return; - } - }); - - afterAll(async () => { - if (redisService.isRedisConnected()) { - await redisService.disconnect(); - } - }); - - beforeEach(async () => { - // Skip tests if Redis is not connected - if (!redisService.isRedisConnected()) { - return; - } - - // Clean up test data - const activeChats = await redisService.getAllActiveChats(); - for (const chat of activeChats) { - if (chat.chatId.startsWith('test-')) { - await redisService.removeActiveChat(chat.chatId); - } - } - - await redisService.removeActiveUser('test-user-1'); - await redisService.removeActiveUser('test-user-2'); - }); - - describe('Active Chat Management', () => { - it('should store and retrieve active chats', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const testChatData = { - chatId: 'test-chat-1', - participants: ['user-1', 'user-2'], - lastActivity: new Date(), - messageCount: 5, - chatType: 'direct' as const, - name: 'Test Chat' - }; - - await redisService.setActiveChat('test-chat-1', testChatData); - const retrieved = await redisService.getActiveChat('test-chat-1'); - - expect(retrieved).toBeDefined(); - expect(retrieved!.chatId).toBe('test-chat-1'); - expect(retrieved!.participants).toEqual(['user-1', 'user-2']); - expect(retrieved!.messageCount).toBe(5); - expect(retrieved!.chatType).toBe('direct'); - expect(retrieved!.name).toBe('Test Chat'); - }); - - it('should return null for non-existent chat', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const retrieved = await redisService.getActiveChat('non-existent-chat'); - expect(retrieved).toBeNull(); - }); - - it('should remove active chats', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const testChatData = { - chatId: 'test-chat-2', - participants: ['user-1', 'user-2'], - lastActivity: new Date(), - messageCount: 0, - chatType: 'group' as const - }; - - await redisService.setActiveChat('test-chat-2', testChatData); - let retrieved = await redisService.getActiveChat('test-chat-2'); - expect(retrieved).toBeDefined(); - - await redisService.removeActiveChat('test-chat-2'); - retrieved = await redisService.getActiveChat('test-chat-2'); - expect(retrieved).toBeNull(); - }); - - it('should update chat activity', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const originalTime = new Date(Date.now() - 60000); // 1 minute ago - const testChatData = { - chatId: 'test-chat-3', - participants: ['user-1', 'user-2'], - lastActivity: originalTime, - messageCount: 5, - chatType: 'direct' as const - }; - - await redisService.setActiveChat('test-chat-3', testChatData); - - // Wait a bit to ensure timestamp difference - await new Promise(resolve => setTimeout(resolve, 10)); - - await redisService.updateChatActivity('test-chat-3', 6); - - const retrieved = await redisService.getActiveChat('test-chat-3'); - expect(retrieved).toBeDefined(); - expect(retrieved!.messageCount).toBe(6); - expect(retrieved!.lastActivity.getTime()).toBeGreaterThan(originalTime.getTime()); - }); - }); - - describe('Active User Management', () => { - it('should store and retrieve active users', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const testUserData = { - userId: 'test-user-1', - activeChatIds: ['chat-1', 'chat-2'], - lastActivity: new Date(), - isOnline: true - }; - - await redisService.setActiveUser('test-user-1', testUserData); - const retrieved = await redisService.getActiveUser('test-user-1'); - - expect(retrieved).toBeDefined(); - expect(retrieved!.userId).toBe('test-user-1'); - expect(retrieved!.activeChatIds).toEqual(['chat-1', 'chat-2']); - expect(retrieved!.isOnline).toBe(true); - }); - - it('should manage user-chat associations', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - // Add user to chats - await redisService.addUserToChat('test-user-2', 'chat-1'); - await redisService.addUserToChat('test-user-2', 'chat-2'); - - let activeChatIds = await redisService.getUserActiveChats('test-user-2'); - expect(activeChatIds).toContain('chat-1'); - expect(activeChatIds).toContain('chat-2'); - - // Remove user from one chat - await redisService.removeUserFromChat('test-user-2', 'chat-1'); - activeChatIds = await redisService.getUserActiveChats('test-user-2'); - expect(activeChatIds).not.toContain('chat-1'); - expect(activeChatIds).toContain('chat-2'); - }); - }); - - describe('Inactive Chat Cleanup', () => { - it('should identify inactive chats', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago - const recentTime = new Date(); - - // Create an inactive chat - await redisService.setActiveChat('test-inactive-chat', { - chatId: 'test-inactive-chat', - participants: ['user-1', 'user-2'], - lastActivity: oldTime, - messageCount: 3, - chatType: 'direct' - }); - - // Create an active chat - await redisService.setActiveChat('test-active-chat', { - chatId: 'test-active-chat', - participants: ['user-1', 'user-3'], - lastActivity: recentTime, - messageCount: 1, - chatType: 'direct' - }); - - const inactiveChats = await redisService.getInactiveChats(60); // 60 minutes - expect(inactiveChats).toContain('test-inactive-chat'); - expect(inactiveChats).not.toContain('test-active-chat'); - - // Cleanup - await redisService.removeActiveChat('test-inactive-chat'); - await redisService.removeActiveChat('test-active-chat'); - }); - - it('should cleanup inactive chats', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000); // 2 hours ago - - await redisService.setActiveChat('test-cleanup-chat', { - chatId: 'test-cleanup-chat', - participants: ['user-1', 'user-2'], - lastActivity: oldTime, - messageCount: 0, - chatType: 'direct' - }); - - const cleanedUp = await redisService.cleanupInactiveChats(60); - expect(cleanedUp).toContain('test-cleanup-chat'); - - // Verify chat was removed - const retrieved = await redisService.getActiveChat('test-cleanup-chat'); - expect(retrieved).toBeNull(); - }); - }); - - describe('Health Check', () => { - it('should ping Redis successfully', async () => { - if (!redisService.isRedisConnected()) { - return; - } - - const pingResult = await redisService.ping(); - expect(pingResult).toBe(true); - }); - - it('should report connection status', () => { - const isConnected = redisService.isRedisConnected(); - expect(typeof isConnected).toBe('boolean'); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/TokenService.test.ts b/SerpentRace_Backend/tests/Application/Services/TokenService.test.ts deleted file mode 100644 index e70c1c84..00000000 --- a/SerpentRace_Backend/tests/Application/Services/TokenService.test.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { TokenService } from '../../../src/Application/Services/TokenService'; -import * as crypto from 'crypto'; - -// Mock crypto module -jest.mock('crypto'); - -describe('TokenService', () => { - let mockRandomBytes: jest.Mock; - let mockCreateHash: jest.Mock; - let mockHashUpdate: jest.Mock; - let mockHashDigest: jest.Mock; - let dateSpy: jest.SpyInstance; - - beforeEach(() => { - jest.clearAllMocks(); - - // Restore Date mock if it exists - if (dateSpy) { - dateSpy.mockRestore(); - } - - mockRandomBytes = jest.mocked(crypto.randomBytes); - mockHashUpdate = jest.fn().mockReturnThis(); - mockHashDigest = jest.fn(); - mockCreateHash = jest.fn().mockReturnValue({ - update: mockHashUpdate, - digest: mockHashDigest - }); - - // Mock crypto.createHash properly - jest.mocked(crypto.createHash).mockImplementation(mockCreateHash); - }); - - afterEach(() => { - // Clean up Date mock - if (dateSpy) { - dateSpy.mockRestore(); - dateSpy = undefined as any; - } - }); - - describe('generateSecureToken', () => { - it('should generate a secure token with default length', () => { - // Arrange - const mockBuffer = { - toString: jest.fn().mockReturnValue('abcdef1234567890') - }; - mockRandomBytes.mockReturnValue(mockBuffer as any); - - // Act - const token = TokenService.generateSecureToken(); - - // Assert - expect(token).toBe('abcdef1234567890'); - expect(mockRandomBytes).toHaveBeenCalledWith(32); - expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); - }); - - it('should generate a secure token with custom length', () => { - // Arrange - const mockBuffer = { - toString: jest.fn().mockReturnValue('abcdef') - }; - mockRandomBytes.mockReturnValue(mockBuffer as any); - - // Act - const token = TokenService.generateSecureToken(16); - - // Assert - expect(token).toBe('abcdef'); - expect(mockRandomBytes).toHaveBeenCalledWith(16); - expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); - }); - - it('should handle crypto errors', () => { - // Arrange - mockRandomBytes.mockImplementation(() => { - throw new Error('Crypto error'); - }); - - // Act & Assert - expect(() => TokenService.generateSecureToken()).toThrow('Failed to generate secure token'); - expect(mockRandomBytes).toHaveBeenCalledWith(32); - }); - }); - - describe('generateVerificationToken', () => { - it('should generate verification token with correct expiration', () => { - // Arrange - const mockBuffer = { - toString: jest.fn().mockReturnValue('verification123') - }; - mockRandomBytes.mockReturnValue(mockBuffer as any); - const mockDate = new Date('2023-01-01T12:00:00Z'); - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any); - - // Act - const result = TokenService.generateVerificationToken(); - - // Assert - expect(result.token).toBe('verification123'); - expect(result.createdAt).toEqual(mockDate); - expect(result.expiresAt).toEqual(new Date('2023-01-02T12:00:00Z')); // 24 hours later - expect(mockRandomBytes).toHaveBeenCalledWith(32); - expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); - }); - - it('should handle token generation errors', () => { - // Arrange - mockRandomBytes.mockImplementation(() => { - throw new Error('Random bytes failed'); - }); - - // Act & Assert - expect(() => TokenService.generateVerificationToken()).toThrow('Failed to generate verification token'); - }); - }); - - describe('generatePasswordResetToken', () => { - it('should generate password reset token with correct expiration', () => { - // Arrange - const mockBuffer = { - toString: jest.fn().mockReturnValue('reset456') - }; - mockRandomBytes.mockReturnValue(mockBuffer as any); - const mockDate = new Date('2023-01-01T12:00:00Z'); - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as any); - - // Act - const result = TokenService.generatePasswordResetToken(); - - // Assert - expect(result.token).toBe('reset456'); - expect(result.createdAt).toEqual(mockDate); - expect(result.expiresAt).toEqual(new Date('2023-01-01T13:00:00Z')); // 1 hour later - expect(mockRandomBytes).toHaveBeenCalledWith(32); - expect(mockBuffer.toString).toHaveBeenCalledWith('hex'); - }); - - it('should handle token generation errors', () => { - // Arrange - mockRandomBytes.mockImplementation(() => { - throw new Error('Random bytes failed'); - }); - - // Act & Assert - expect(() => TokenService.generatePasswordResetToken()).toThrow('Failed to generate password reset token'); - }); - }); - - describe('hashToken', () => { - it('should hash token correctly', async () => { - // Arrange - const token = 'test-token-123'; - const hashedToken = 'hashed-token-result'; - mockHashDigest.mockReturnValue(hashedToken); - - // Act - const result = await TokenService.hashToken(token); - - // Assert - expect(result).toBe(hashedToken); - expect(mockCreateHash).toHaveBeenCalledWith('sha256'); - expect(mockHashUpdate).toHaveBeenCalledWith(token); - expect(mockHashDigest).toHaveBeenCalledWith('hex'); - }); - - it('should handle hashing errors', async () => { - // Arrange - const token = 'test-token-123'; - mockCreateHash.mockImplementation(() => { - throw new Error('Hashing failed'); - }); - - // Act & Assert - await expect(TokenService.hashToken(token)).rejects.toThrow('Failed to hash token'); - }); - }); - - describe('verifyToken', () => { - it('should return true when tokens match', async () => { - // Arrange - const plainToken = 'plain-token'; - const hashedToken = 'expected-hash'; - mockHashDigest.mockReturnValue(hashedToken); - - // Act - const result = await TokenService.verifyToken(plainToken, hashedToken); - - // Assert - expect(result).toBe(true); - expect(mockCreateHash).toHaveBeenCalledWith('sha256'); - expect(mockHashUpdate).toHaveBeenCalledWith(plainToken); - expect(mockHashDigest).toHaveBeenCalledWith('hex'); - }); - - it('should return false when tokens do not match', async () => { - // Arrange - const plainToken = 'plain-token'; - const hashedToken = 'expected-hash'; - const actualHash = 'different-hash'; - mockHashDigest.mockReturnValue(actualHash); - - // Act - const result = await TokenService.verifyToken(plainToken, hashedToken); - - // Assert - expect(result).toBe(false); - expect(mockCreateHash).toHaveBeenCalledWith('sha256'); - expect(mockHashUpdate).toHaveBeenCalledWith(plainToken); - expect(mockHashDigest).toHaveBeenCalledWith('hex'); - }); - - it('should handle verification errors', async () => { - // Arrange - const plainToken = 'plain-token'; - const hashedToken = 'expected-hash'; - mockCreateHash.mockImplementation(() => { - throw new Error('Hash creation failed'); - }); - - // Act & Assert - TokenService.verifyToken catches errors and returns false, doesn't throw - const result = await TokenService.verifyToken(plainToken, hashedToken); - expect(result).toBe(false); - }); - }); - - describe('isTokenExpired', () => { - it('should return false for non-expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const futureDate = new Date('2023-01-01T13:00:00Z'); // 1 hour from now - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.isTokenExpired(futureDate); - - // Assert - expect(result).toBe(false); - - // Cleanup - dateSpy.mockRestore(); - }); - - it('should return true for expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const pastDate = new Date('2023-01-01T11:00:00Z'); // 1 hour ago - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.isTokenExpired(pastDate); - - // Assert - expect(result).toBe(true); - - // Cleanup - dateSpy.mockRestore(); - }); - - it('should return true for exactly expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const exactlyNow = new Date('2023-01-01T12:00:00Z'); - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.isTokenExpired(exactlyNow); - - // Assert - expect(result).toBe(false); // new Date() > expiresAt is false when they're equal - - // Cleanup - dateSpy.mockRestore(); - }); - }); - - describe('generateTokenWithExpiry', () => { - it('should validate token format correctly', () => { - // Arrange - valid hex token with expected length (64 chars for 32 bytes) - const validToken = 'a'.repeat(64); // 64 hex characters - - // Act - const result = TokenService.isValidTokenFormat(validToken); - - // Assert - expect(result).toBe(true); - }); - - it('should reject invalid token format', () => { - // Arrange - const invalidTokens = [ - '', // empty - 'invalid-token-with-dashes', // non-hex characters - 'abc123', // too short - null as any, // null - undefined as any, // undefined - 123 as any // not string - ]; - - invalidTokens.forEach(invalidToken => { - // Act - const result = TokenService.isValidTokenFormat(invalidToken); - - // Assert - expect(result).toBe(false); - }); - }); - }); - - describe('generateVerificationUrl', () => { - it('should generate correct verification URL', () => { - // Arrange - const baseUrl = 'https://example.com'; - const token = 'verification-token-123'; - - // Act - const url = TokenService.generateVerificationUrl(baseUrl, token); - - // Assert - expect(url).toBe('https://example.com/verify-email?token=verification-token-123'); - }); - - it('should handle base URL with trailing slash', () => { - // Arrange - const baseUrl = 'https://example.com/'; - const token = 'verification-token-123'; - - // Act - const url = TokenService.generateVerificationUrl(baseUrl, token); - - // Assert - expect(url).toBe('https://example.com/verify-email?token=verification-token-123'); - }); - - it('should encode special characters in token', () => { - // Arrange - const baseUrl = 'https://example.com'; - const token = 'token+with/special=chars'; - - // Act - const url = TokenService.generateVerificationUrl(baseUrl, token); - - // Assert - expect(url).toContain(encodeURIComponent(token)); - }); - }); - - describe('generatePasswordResetUrl', () => { - it('should generate correct password reset URL', () => { - // Arrange - const baseUrl = 'https://example.com'; - const token = 'reset-token-456'; - - // Act - const url = TokenService.generatePasswordResetUrl(baseUrl, token); - - // Assert - expect(url).toBe('https://example.com/reset-password?token=reset-token-456'); - }); - }); - - describe('getExpirationInfo', () => { - it('should return correct info for non-expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const futureDate = new Date('2023-01-01T14:00:00Z'); // 2 hours from now - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.getExpirationInfo(futureDate); - - // Assert - expect(result.expired).toBe(false); - expect(result.timeLeft).toContain('Expires in'); - expect(result.timeLeft).toContain('hour(s)'); - - // Cleanup - dateSpy.mockRestore(); - }); - - it('should return correct info for expired token', () => { - // Arrange - const currentTime = new Date('2023-01-01T12:00:00Z'); - const pastDate = new Date('2023-01-01T11:30:00Z'); // 30 minutes ago - - dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => currentTime as any); - - // Act - const result = TokenService.getExpirationInfo(pastDate); - - // Assert - expect(result.expired).toBe(true); - expect(result.timeLeft).toContain('Expired'); - expect(result.timeLeft).toContain('minute(s) ago'); - - // Cleanup - dateSpy.mockRestore(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/Services/ValidationMiddleware.test.ts b/SerpentRace_Backend/tests/Application/Services/ValidationMiddleware.test.ts deleted file mode 100644 index 443688cc..00000000 --- a/SerpentRace_Backend/tests/Application/Services/ValidationMiddleware.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { ValidationMiddleware } from '../../../src/Application/Services/ValidationMiddleware'; -import { Request, Response, NextFunction } from 'express'; -import { ErrorResponseService } from '../../../src/Application/Services/ErrorResponseService'; - -jest.mock('../../../src/Application/Services/ErrorResponseService'); -jest.mock('../../../src/Application/Services/Logger'); - -describe('ValidationMiddleware', () => { - let req: Partial; - let res: Partial; - let next: NextFunction; - - beforeEach(() => { - req = { - body: {}, - params: {}, - query: {}, - path: '/test' - }; - res = { - status: jest.fn().mockReturnThis(), - json: jest.fn().mockReturnThis() - }; - next = jest.fn(); - jest.clearAllMocks(); - }); - - describe('validateRequiredFields', () => { - it('should pass validation when all required fields are present', () => { - req.body = { username: 'testuser', email: 'test@example.com' }; - - const middleware = ValidationMiddleware.validateRequiredFields(['username', 'email']); - middleware(req as Request, res as Response, next); - - expect(next).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - }); - - it('should fail validation when required fields are missing', () => { - req.body = { username: 'testuser' }; // missing email - - const middleware = ValidationMiddleware.validateRequiredFields(['username', 'email']); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'Missing required fields', - { missingFields: ['email'] } - ); - expect(next).not.toHaveBeenCalled(); - }); - - it('should fail validation when fields are empty strings', () => { - req.body = { username: '', email: 'test@example.com' }; - - const middleware = ValidationMiddleware.validateRequiredFields(['username', 'email']); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'Missing required fields', - { missingFields: ['username'] } - ); - }); - }); - - describe('validateEmailFormat', () => { - it('should pass validation for valid email', () => { - req.body = { email: 'test@example.com' }; - - const middleware = ValidationMiddleware.validateEmailFormat(['email']); - middleware(req as Request, res as Response, next); - - expect(next).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - }); - - it('should fail validation for invalid email', () => { - req.body = { email: 'invalid-email' }; - - const middleware = ValidationMiddleware.validateEmailFormat(['email']); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'Email format validation failed', - { errors: ["Field 'email' must contain a valid email address"] } - ); - expect(next).not.toHaveBeenCalled(); - }); - }); - - describe('validateUUIDFormat', () => { - it('should pass validation for valid UUID', () => { - req.params = { userId: '123e4567-e89b-12d3-a456-426614174000' }; - - const middleware = ValidationMiddleware.validateUUIDFormat(['userId']); - middleware(req as Request, res as Response, next); - - expect(next).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - }); - - it('should fail validation for invalid UUID', () => { - req.params = { userId: 'invalid-uuid' }; - - const middleware = ValidationMiddleware.validateUUIDFormat(['userId']); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'UUID format validation failed', - { errors: ["Field 'userId' must contain a valid UUID"] } - ); - expect(next).not.toHaveBeenCalled(); - }); - }); - - describe('validateStringLength', () => { - it('should pass validation for strings within length constraints', () => { - req.body = { username: 'testuser', password: 'password123' }; - - const middleware = ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 20 }, - password: { min: 8, max: 50 } - }); - middleware(req as Request, res as Response, next); - - expect(next).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - }); - - it('should fail validation for strings that are too short', () => { - req.body = { username: 'ab' }; // too short (min 3) - - const middleware = ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 20 } - }); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'String length validation failed', - { errors: ["Field 'username' must be at least 3 characters"] } - ); - }); - - it('should fail validation for strings that are too long', () => { - req.body = { username: 'a'.repeat(25) }; // too long (max 20) - - const middleware = ValidationMiddleware.validateStringLength({ - username: { min: 3, max: 20 } - }); - middleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'String length validation failed', - { errors: ["Field 'username' must not exceed 20 characters"] } - ); - }); - }); - - describe('combine', () => { - it('should run all validations in sequence and pass if all succeed', (done) => { - req.body = { username: 'testuser', email: 'test@example.com' }; - - const nextSpy = jest.fn(() => { - try { - expect(nextSpy).toHaveBeenCalledWith(); - expect(ErrorResponseService.sendBadRequest).not.toHaveBeenCalled(); - done(); - } catch (error) { - done(error); - } - }); - - const combinedMiddleware = ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['username', 'email']), - ValidationMiddleware.validateEmailFormat(['email']), - ValidationMiddleware.validateStringLength({ username: { min: 3, max: 20 } }) - ]); - - combinedMiddleware(req as Request, res as Response, nextSpy); - }); - - it('should stop at first validation failure', () => { - req.body = { username: 'testuser' }; // missing email - - const combinedMiddleware = ValidationMiddleware.combine([ - ValidationMiddleware.validateRequiredFields(['username', 'email']), - ValidationMiddleware.validateEmailFormat(['email']), // this won't run - ValidationMiddleware.validateStringLength({ username: { min: 3, max: 20 } }) // this won't run - ]); - - combinedMiddleware(req as Request, res as Response, next); - - expect(ErrorResponseService.sendBadRequest).toHaveBeenCalledWith( - res, - 'Missing required fields', - { missingFields: ['email'] } - ); - expect(next).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/Application/User/commands/UserCommandHandlers.comprehensive.test.ts b/SerpentRace_Backend/tests/Application/User/commands/UserCommandHandlers.comprehensive.test.ts deleted file mode 100644 index 42c92a51..00000000 --- a/SerpentRace_Backend/tests/Application/User/commands/UserCommandHandlers.comprehensive.test.ts +++ /dev/null @@ -1,430 +0,0 @@ -// Comprehensive test coverage for User Command Handlers -import { CreateUserCommand } from '../../../../src/Application/User/commands/CreateUserCommand'; -import { CreateUserCommandHandler } from '../../../../src/Application/User/commands/CreateUserCommandHandler'; -import { LoginCommand } from '../../../../src/Application/User/commands/LoginCommand'; -import { LoginCommandHandler } from '../../../../src/Application/User/commands/LoginCommandHandler'; -import { UpdateUserCommand } from '../../../../src/Application/User/commands/UpdateUserCommand'; -import { UpdateUserCommandHandler } from '../../../../src/Application/User/commands/UpdateUserCommandHandler'; -import { DeactivateUserCommand } from '../../../../src/Application/User/commands/DeactivateUserCommand'; -import { DeactivateUserCommandHandler } from '../../../../src/Application/User/commands/DeactivateUserCommandHandler'; -import { IUserRepository } from '../../../../src/Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../../../../src/Domain/IRepository/IOrganizationRepository'; -import { JWTService } from '../../../../src/Application/Services/JWTService'; -import { PasswordService } from '../../../../src/Application/Services/PasswordService'; -import { UserState } from '../../../../src/Domain/User/UserAggregate'; -import { - createMockUser, - createMockUserRepository, - createMockOrganizationRepository, - createMockJWTService -} from '../../../testUtils'; - -// Mock PasswordService static methods -jest.mock('../../../../src/Application/Services/PasswordService', () => ({ - PasswordService: { - validatePasswordStrength: jest.fn().mockReturnValue({ isValid: true, errors: [] }), - hashPassword: jest.fn().mockResolvedValue('hashed-password'), - verifyPassword: jest.fn().mockResolvedValue(true) - } -})); - -describe('User Command Handlers - Comprehensive Coverage', () => { - describe('CreateUserCommandHandler', () => { - let mockUserRepository: jest.Mocked; - let handler: CreateUserCommandHandler; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - handler = new CreateUserCommandHandler(mockUserRepository); - }); - - it('should create a new user successfully', async () => { - // Arrange - const command: CreateUserCommand = { - username: 'testuser', - email: 'test@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - const mockUser = createMockUser({ - username: command.username, - email: command.email, - state: UserState.REGISTERED_NOT_VERIFIED - }); - - // CreateUserCommandHandler doesn't check existing users - goes directly to create - mockUserRepository.create.mockResolvedValue(mockUser); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - // CreateUserCommandHandler doesn't call findByUsername/findByEmail - expect(mockUserRepository.create).toHaveBeenCalled(); - }); - - it('should throw error when username already exists', async () => { - // Arrange - const command: CreateUserCommand = { - username: 'existinguser', - email: 'test@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - // Simulate database constraint error for duplicate username - mockUserRepository.create.mockRejectedValue(new Error('duplicate key value violates unique constraint')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('User with this username or email already exists'); - }); - - it('should throw error when email already exists', async () => { - // Arrange - const command: CreateUserCommand = { - username: 'testuser', - email: 'existing@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - // Simulate database constraint error for duplicate email - mockUserRepository.create.mockRejectedValue(new Error('unique constraint violation')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('User with this username or email already exists'); - }); - - it('should handle repository errors', async () => { - // Arrange - const command: CreateUserCommand = { - username: 'testuser', - email: 'test@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - mockUserRepository.findByUsername.mockResolvedValue(null); - mockUserRepository.findByEmail.mockResolvedValue(null); - mockUserRepository.create.mockRejectedValue(new Error('Database error')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Failed to create user'); - }); - }); - - describe('LoginCommandHandler', () => { - let mockUserRepository: jest.Mocked; - let mockOrgRepository: jest.Mocked; - let mockJwtService: jest.Mocked; - let handler: LoginCommandHandler; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - mockOrgRepository = createMockOrganizationRepository(); - mockJwtService = createMockJWTService(); - handler = new LoginCommandHandler(mockUserRepository, mockJwtService, mockOrgRepository); - - // Reset all mocks - jest.clearAllMocks(); - - // Set default PasswordService behavior - const mockPasswordService = PasswordService as jest.Mocked; - mockPasswordService.verifyPassword.mockResolvedValue(true); // Default to valid password - }); - - it('should login user with valid credentials', async () => { - // Arrange - const command: LoginCommand = { - username: 'testuser', - password: 'Password123!' - }; - - const mockUser = createMockUser({ - username: command.username, - state: UserState.VERIFIED_REGULAR - }); - - mockUserRepository.findByUsername.mockResolvedValue(mockUser); - mockJwtService.create.mockReturnValue('jwt-token'); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(result!.token).toBe('jwt-token'); - expect(mockJwtService.create).toHaveBeenCalled(); - }); - - it('should handle user not found', async () => { - // Arrange - const command: LoginCommand = { - username: 'nonexistent', - password: 'password123' - }; - - mockUserRepository.findByUsername.mockResolvedValue(null); - - // Act & Assert - const result = await handler.execute(command); - expect(result).toBeNull(); - }); - - it('should handle invalid password', async () => { - // Arrange - const command: LoginCommand = { - username: 'testuser', - password: 'wrongpassword' - }; - - const mockUser = createMockUser({ - username: command.username, - password: 'hashedpassword' - }); - - mockUserRepository.findByUsername.mockResolvedValue(mockUser); - - // Mock password verification to return false for wrong password - const mockPasswordService = PasswordService as jest.Mocked; - mockPasswordService.verifyPassword.mockResolvedValue(false); - - // Act & Assert - const result = await handler.execute(command); - expect(result).toBeNull(); - }); - - it('should handle unverified user', async () => { - // Arrange - LoginCommandHandler doesn't reject unverified users, it processes them normally - const command: LoginCommand = { - username: 'testuser', - password: 'Password123!' - }; - - const mockUser = createMockUser({ - username: command.username, - password: 'hashedpassword', - state: UserState.REGISTERED_NOT_VERIFIED - }); - - mockUserRepository.findByUsername.mockResolvedValue(mockUser); - mockJwtService.create.mockReturnValue('jwt-token'); - - // Act - const result = await handler.execute(command); - - // Assert - LoginCommandHandler processes unverified users normally - expect(result).toBeDefined(); - expect(result!.user).toBeDefined(); - expect(result!.token).toBe('jwt-token'); - }); - }); - - describe('UpdateUserCommandHandler', () => { - let mockUserRepository: jest.Mocked; - let handler: UpdateUserCommandHandler; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - handler = new UpdateUserCommandHandler(mockUserRepository); - }); - - it('should update user successfully', async () => { - // Arrange - const command: UpdateUserCommand = { - id: 'user-123', - email: 'newemail@example.com' - }; - - const existingUser = createMockUser({ id: command.id }); - const updatedUser = createMockUser({ - id: command.id, - email: command.email - }); - - mockUserRepository.findById.mockResolvedValue(existingUser); - mockUserRepository.update.mockResolvedValue(updatedUser); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - expect(mockUserRepository.update).toHaveBeenCalledWith(command.id, expect.any(Object)); - }); - - it('should return null when user not found', async () => { - // Arrange - const command: UpdateUserCommand = { - id: 'nonexistent-user', - email: 'newemail@example.com' - }; - - mockUserRepository.update.mockResolvedValue(null); // UpdateUserCommandHandler calls update directly, not findById first - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeNull(); - expect(mockUserRepository.update).toHaveBeenCalledWith(command.id, expect.any(Object)); - }); - - it('should handle partial updates', async () => { - // Arrange - const command: UpdateUserCommand = { - id: 'user-123', - username: 'newusername' - }; - - const existingUser = createMockUser({ id: command.id }); - const updatedUser = createMockUser({ - id: command.id, - username: command.username - }); - - mockUserRepository.findById.mockResolvedValue(existingUser); - mockUserRepository.update.mockResolvedValue(updatedUser); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBeDefined(); - }); - }); - - describe('DeactivateUserCommandHandler', () => { - let mockUserRepository: jest.Mocked; - let handler: DeactivateUserCommandHandler; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - handler = new DeactivateUserCommandHandler(mockUserRepository); - }); - - it('should deactivate user successfully', async () => { - // Arrange - const command: DeactivateUserCommand = { - id: 'user-123' - }; - - const deactivatedUser = createMockUser({ - id: command.id, - state: UserState.DEACTIVATED - }); - - mockUserRepository.deactivate.mockResolvedValue(deactivatedUser); - - // Act - const result = await handler.execute(command); - - // Assert - expect(result).toBe(true); - expect(mockUserRepository.deactivate).toHaveBeenCalledWith(command.id); - }); - - it('should handle repository errors', async () => { - // Arrange - const command: DeactivateUserCommand = { - id: 'user-123' - }; - - mockUserRepository.deactivate.mockRejectedValue(new Error('Deactivation failed')); - - // Act & Assert - await expect(handler.execute(command)).rejects.toThrow('Deactivation failed'); - }); - }); - - describe('Cross-Command Integration Tests', () => { - let mockUserRepository: jest.Mocked; - let mockOrgRepository: jest.Mocked; - let mockJwtService: jest.Mocked; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - mockOrgRepository = createMockOrganizationRepository(); - mockJwtService = createMockJWTService(); - }); - - it('should create user and then login', async () => { - // Arrange - const createHandler = new CreateUserCommandHandler(mockUserRepository); - const loginHandler = new LoginCommandHandler(mockUserRepository, mockJwtService, mockOrgRepository); - - const createCommand: CreateUserCommand = { - username: 'testuser', - email: 'test@example.com', - password: 'Password123!', // Strong password - fname: 'Test', - lname: 'User', - type: 'regular' - }; - - const loginCommand: LoginCommand = { - username: 'testuser', - password: 'Password123!' // Strong password - }; - - const mockUser = createMockUser({ - username: createCommand.username, - email: createCommand.email, - state: UserState.VERIFIED_REGULAR - }); - - // Mock create user flow - mockUserRepository.findByUsername.mockResolvedValueOnce(null); - mockUserRepository.findByEmail.mockResolvedValue(null); - mockUserRepository.create.mockResolvedValue(mockUser); - - // Mock login flow - mockUserRepository.findByUsername.mockResolvedValueOnce(mockUser); - mockJwtService.create.mockReturnValue('jwt-token'); - - // Act - const createResult = await createHandler.execute(createCommand); - const loginResult = await loginHandler.execute(loginCommand); - - // Assert - expect(createResult).toBeDefined(); - expect(loginResult).toBeDefined(); - }); - - it('should update user after creation', async () => { - // Arrange - const updateHandler = new UpdateUserCommandHandler(mockUserRepository); - - const updateCommand: UpdateUserCommand = { - id: 'user-123', - email: 'updated@example.com' - }; - - const existingUser = createMockUser({ id: updateCommand.id }); - const updatedUser = createMockUser({ - id: updateCommand.id, - email: updateCommand.email - }); - - mockUserRepository.findById.mockResolvedValue(existingUser); - mockUserRepository.update.mockResolvedValue(updatedUser); - - // Act - const result = await updateHandler.execute(updateCommand); - - // Assert - expect(result).toBeDefined(); - expect(result).not.toBeNull(); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/comprehensive-repository-coverage.test.ts b/SerpentRace_Backend/tests/comprehensive-repository-coverage.test.ts deleted file mode 100644 index 2bbdaa39..00000000 --- a/SerpentRace_Backend/tests/comprehensive-repository-coverage.test.ts +++ /dev/null @@ -1,286 +0,0 @@ -// Comprehensive test coverage for Repository layer -import { IUserRepository } from '../src/Domain/IRepository/IUserRepository'; -import { IDeckRepository } from '../src/Domain/IRepository/IDeckRepository'; -import { IOrganizationRepository } from '../src/Domain/IRepository/IOrganizationRepository'; -import { IContactRepository } from '../src/Domain/IRepository/IContactRepository'; -import { UserAggregate, UserState } from '../src/Domain/User/UserAggregate'; -import { DeckAggregate, Type as DeckType } from '../src/Domain/Deck/DeckAggregate'; -import { OrganizationAggregate } from '../src/Domain/Organization/OrganizationAggregate'; -import { ContactAggregate } from '../src/Domain/Contact/ContactAggregate'; -import { - createMockUser, - createMockDeck, - createMockOrganization, - createMockContact, - createMockUserRepository, - createMockDeckRepository, - createMockOrganizationRepository, - createMockContactRepository -} from './testUtils'; - -describe('Repository Layer - Comprehensive Coverage', () => { - describe('IUserRepository Interface Coverage', () => { - let mockUserRepository: jest.Mocked; - - beforeEach(() => { - mockUserRepository = createMockUserRepository(); - }); - - it('should implement all required methods', () => { - expect(mockUserRepository.create).toBeDefined(); - expect(mockUserRepository.findByPage).toBeDefined(); - expect(mockUserRepository.findByPageIncludingDeleted).toBeDefined(); - expect(mockUserRepository.findById).toBeDefined(); - expect(mockUserRepository.findByIdIncludingDeleted).toBeDefined(); - expect(mockUserRepository.findByUsername).toBeDefined(); - expect(mockUserRepository.findByEmail).toBeDefined(); - expect(mockUserRepository.findByToken).toBeDefined(); - expect(mockUserRepository.search).toBeDefined(); - expect(mockUserRepository.searchIncludingDeleted).toBeDefined(); - expect(mockUserRepository.update).toBeDefined(); - expect(mockUserRepository.delete).toBeDefined(); - expect(mockUserRepository.softDelete).toBeDefined(); - expect(mockUserRepository.deactivate).toBeDefined(); - }); - - it('should handle user creation', async () => { - const userData = { username: 'testuser', email: 'test@example.com' }; - const mockUser = createMockUser(userData); - mockUserRepository.create.mockResolvedValue(mockUser); - - const result = await mockUserRepository.create(userData); - expect(result).toEqual(mockUser); - expect(mockUserRepository.create).toHaveBeenCalledWith(userData); - }); - - it('should handle paginated user retrieval', async () => { - const mockUsers = [createMockUser(), createMockUser({ id: 'user2' })]; - mockUserRepository.findByPage.mockResolvedValue({ users: mockUsers, totalCount: 2 }); - - const result = await mockUserRepository.findByPage(0, 10); - expect(result.users).toHaveLength(2); - expect(result.totalCount).toBe(2); - }); - - it('should handle user search operations', async () => { - const mockUsers = [createMockUser({ username: 'searchtest' })]; - mockUserRepository.search.mockResolvedValue({ users: mockUsers, totalCount: 1 }); - - const result = await mockUserRepository.search('searchtest'); - expect(result.users).toHaveLength(1); - expect(result.users[0].username).toBe('searchtest'); - }); - - it('should handle user state transitions', async () => { - const mockUser = createMockUser({ state: UserState.VERIFIED_REGULAR }); - mockUserRepository.deactivate.mockResolvedValue(mockUser); - - const result = await mockUserRepository.deactivate('user-id'); - expect(result).toEqual(mockUser); - }); - }); - - describe('IDeckRepository Interface Coverage', () => { - let mockDeckRepository: jest.Mocked; - - beforeEach(() => { - mockDeckRepository = createMockDeckRepository(); - }); - - it('should implement all required methods including new ones', () => { - expect(mockDeckRepository.create).toBeDefined(); - expect(mockDeckRepository.findByPage).toBeDefined(); - expect(mockDeckRepository.findByPageIncludingDeleted).toBeDefined(); - expect(mockDeckRepository.findById).toBeDefined(); - expect(mockDeckRepository.findByIdIncludingDeleted).toBeDefined(); - expect(mockDeckRepository.search).toBeDefined(); - expect(mockDeckRepository.searchIncludingDeleted).toBeDefined(); - expect(mockDeckRepository.update).toBeDefined(); - expect(mockDeckRepository.delete).toBeDefined(); - expect(mockDeckRepository.softDelete).toBeDefined(); - expect(mockDeckRepository.countActiveByUserId).toBeDefined(); - expect(mockDeckRepository.countOrganizationalByUserId).toBeDefined(); - expect(mockDeckRepository.findFilteredDecks).toBeDefined(); - }); - - it('should handle deck counting operations', async () => { - mockDeckRepository.countActiveByUserId.mockResolvedValue(5); - mockDeckRepository.countOrganizationalByUserId.mockResolvedValue(3); - - const activeCount = await mockDeckRepository.countActiveByUserId('user-id'); - const orgCount = await mockDeckRepository.countOrganizationalByUserId('user-id'); - - expect(activeCount).toBe(5); - expect(orgCount).toBe(3); - }); - - it('should handle filtered deck retrieval', async () => { - const mockDecks = [createMockDeck(), createMockDeck({ id: 'deck2' })]; - mockDeckRepository.findFilteredDecks.mockResolvedValue({ decks: mockDecks, totalCount: 2 }); - - const result = await mockDeckRepository.findFilteredDecks('user-id', 'org-id', false, 0, 10); - expect(result.decks).toHaveLength(2); - expect(result.totalCount).toBe(2); - }); - - it('should handle different deck types', async () => { - const jokerDeck = createMockDeck({ type: DeckType.JOKER }); - const luckDeck = createMockDeck({ type: DeckType.LUCK }); - const questionDeck = createMockDeck({ type: DeckType.QUESTION }); - - mockDeckRepository.create.mockResolvedValueOnce(jokerDeck); - mockDeckRepository.create.mockResolvedValueOnce(luckDeck); - mockDeckRepository.create.mockResolvedValueOnce(questionDeck); - - const result1 = await mockDeckRepository.create({ type: DeckType.JOKER }); - const result2 = await mockDeckRepository.create({ type: DeckType.LUCK }); - const result3 = await mockDeckRepository.create({ type: DeckType.QUESTION }); - - expect(result1.type).toBe(DeckType.JOKER); - expect(result2.type).toBe(DeckType.LUCK); - expect(result3.type).toBe(DeckType.QUESTION); - }); - }); - - describe('IOrganizationRepository Interface Coverage', () => { - let mockOrgRepository: jest.Mocked; - - beforeEach(() => { - mockOrgRepository = createMockOrganizationRepository(); - }); - - it('should implement all required methods', () => { - expect(mockOrgRepository.create).toBeDefined(); - expect(mockOrgRepository.findByPage).toBeDefined(); - expect(mockOrgRepository.findByPageIncludingDeleted).toBeDefined(); - expect(mockOrgRepository.findById).toBeDefined(); - expect(mockOrgRepository.findByIdIncludingDeleted).toBeDefined(); - expect(mockOrgRepository.search).toBeDefined(); - expect(mockOrgRepository.searchIncludingDeleted).toBeDefined(); - expect(mockOrgRepository.update).toBeDefined(); - expect(mockOrgRepository.delete).toBeDefined(); - expect(mockOrgRepository.softDelete).toBeDefined(); - }); - - it('should handle organization CRUD operations', async () => { - const orgData = { name: 'Test Org', contactemail: 'test@org.com' }; - const mockOrg = createMockOrganization(orgData); - - mockOrgRepository.create.mockResolvedValue(mockOrg); - mockOrgRepository.findById.mockResolvedValue(mockOrg); - mockOrgRepository.update.mockResolvedValue(mockOrg); - mockOrgRepository.softDelete.mockResolvedValue(mockOrg); - - const created = await mockOrgRepository.create(orgData); - const found = await mockOrgRepository.findById('org-id'); - const updated = await mockOrgRepository.update('org-id', { name: 'Updated Org' }); - const deleted = await mockOrgRepository.softDelete('org-id'); - - expect(created.name).toBe('Test Org'); - expect(found).toEqual(mockOrg); - expect(updated).toEqual(mockOrg); - expect(deleted).toEqual(mockOrg); - }); - }); - - describe('IContactRepository Interface Coverage', () => { - let mockContactRepository: jest.Mocked; - - beforeEach(() => { - mockContactRepository = createMockContactRepository(); - }); - - it('should implement all required methods', () => { - expect(mockContactRepository.create).toBeDefined(); - expect(mockContactRepository.findById).toBeDefined(); - expect(mockContactRepository.findByPage).toBeDefined(); - expect(mockContactRepository.findByPageIncludingDeleted).toBeDefined(); - expect(mockContactRepository.findByIdIncludingDeleted).toBeDefined(); - expect(mockContactRepository.search).toBeDefined(); - expect(mockContactRepository.searchIncludingDeleted).toBeDefined(); - expect(mockContactRepository.update).toBeDefined(); - expect(mockContactRepository.delete).toBeDefined(); - expect(mockContactRepository.softDelete).toBeDefined(); - }); - - it('should handle contact search operations', async () => { - const mockContacts = [createMockContact({ email: 'test@example.com' })]; - mockContactRepository.search.mockResolvedValue(mockContacts); - mockContactRepository.searchIncludingDeleted.mockResolvedValue(mockContacts); - - const activeResults = await mockContactRepository.search('test'); - const allResults = await mockContactRepository.searchIncludingDeleted('test'); - - expect(activeResults).toHaveLength(1); - expect(allResults).toHaveLength(1); - }); - - it('should handle contact lifecycle', async () => { - const contactData = { email: 'user@example.com', message: 'Help Request' }; - const mockContact = createMockContact(contactData); - - mockContactRepository.create.mockResolvedValue(mockContact); - mockContactRepository.findById.mockResolvedValue(mockContact); - mockContactRepository.findByIdIncludingDeleted.mockResolvedValue(mockContact); - - const created = await mockContactRepository.create(contactData); - const found = await mockContactRepository.findById('contact-id'); - const foundWithDeleted = await mockContactRepository.findByIdIncludingDeleted('contact-id'); - - expect(created.email).toBe('user@example.com'); - expect(found).toEqual(mockContact); - expect(foundWithDeleted).toEqual(mockContact); - }); - }); - - describe('Cross-Repository Integration Tests', () => { - let userRepo: jest.Mocked; - let deckRepo: jest.Mocked; - let orgRepo: jest.Mocked; - - beforeEach(() => { - userRepo = createMockUserRepository(); - deckRepo = createMockDeckRepository(); - orgRepo = createMockOrganizationRepository(); - }); - - it('should simulate user-deck relationship operations', async () => { - const mockUser = createMockUser({ id: 'user-123' }); - const mockDecks = [ - createMockDeck({ userid: 'user-123', name: 'Deck 1' }), - createMockDeck({ userid: 'user-123', name: 'Deck 2' }) - ]; - - userRepo.findById.mockResolvedValue(mockUser); - deckRepo.findFilteredDecks.mockResolvedValue({ decks: mockDecks, totalCount: 2 }); - deckRepo.countActiveByUserId.mockResolvedValue(2); - - const user = await userRepo.findById('user-123'); - const userDecks = await deckRepo.findFilteredDecks('user-123'); - const deckCount = await deckRepo.countActiveByUserId('user-123'); - - expect(user).toBeDefined(); - expect(userDecks.decks).toHaveLength(2); - expect(deckCount).toBe(2); - expect(userDecks.decks.every(deck => deck.userid === 'user-123')).toBe(true); - }); - - it('should simulate organization-user relationship operations', async () => { - const mockOrg = createMockOrganization({ id: 'org-123', name: 'Test Organization' }); - const mockUsers = [ - createMockUser({ orgid: 'org-123' }), - createMockUser({ orgid: 'org-123', id: 'user-2' }) - ]; - - orgRepo.findById.mockResolvedValue(mockOrg); - userRepo.findByPage.mockResolvedValue({ users: mockUsers, totalCount: 2 }); - - const org = await orgRepo.findById('org-123'); - const orgUsers = await userRepo.findByPage(0, 10); - - expect(org).toBeDefined(); - expect(orgUsers.users).toHaveLength(2); - expect(orgUsers.users.every(user => user.orgid === 'org-123')).toBe(true); - }); - }); -}); diff --git a/SerpentRace_Backend/tests/jest.setup.ts b/SerpentRace_Backend/tests/jest.setup.ts deleted file mode 100644 index 9484f3b9..00000000 --- a/SerpentRace_Backend/tests/jest.setup.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Set the NODE_ENV to test for all Jest tests -process.env.NODE_ENV = 'test'; diff --git a/SerpentRace_Backend/tests/setup.ts b/SerpentRace_Backend/tests/setup.ts deleted file mode 100644 index 7cf900aa..00000000 --- a/SerpentRace_Backend/tests/setup.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Jest test setup file -import { jest } from '@jest/globals'; -import { LoggingService } from '../src/Application/Services/LoggingService'; - -// Mock environment variables -process.env.NODE_ENV = 'test'; -process.env.JWT_SECRET = 'test-jwt-secret'; -process.env.EMAIL_HOST = 'test.smtp.com'; -process.env.EMAIL_PORT = '587'; -process.env.EMAIL_USER = 'test@example.com'; -process.env.EMAIL_PASS = 'testpass'; -process.env.EMAIL_FROM = 'test@example.com'; -process.env.APP_BASE_URL = 'http://localhost:3000'; - -// Global test timeout -jest.setTimeout(10000); - -// Global cleanup to prevent Jest from hanging -afterAll(async () => { - try { - await LoggingService.getInstance().shutdown(); - } catch (error) { - // Ignore cleanup errors in tests - console.log('Test cleanup completed'); - } -}); diff --git a/SerpentRace_Backend/tests/testUtils.ts b/SerpentRace_Backend/tests/testUtils.ts deleted file mode 100644 index 4f759a44..00000000 --- a/SerpentRace_Backend/tests/testUtils.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { UserAggregate, UserState } from '../src/Domain/User/UserAggregate'; -import { OrganizationAggregate, OrganizationState } from '../src/Domain/Organization/OrganizationAggregate'; -import { DeckAggregate, State as DeckState, Type as DeckType, CType } from '../src/Domain/Deck/DeckAggregate'; -import { ContactAggregate, ContactState, ContactType } from '../src/Domain/Contact/ContactAggregate'; -import { IUserRepository } from '../src/Domain/IRepository/IUserRepository'; -import { IOrganizationRepository } from '../src/Domain/IRepository/IOrganizationRepository'; -import { IDeckRepository } from '../src/Domain/IRepository/IDeckRepository'; -import { IContactRepository } from '../src/Domain/IRepository/IContactRepository'; - -export const createMockUser = (overrides: Partial = {}): UserAggregate => ({ - id: '123e4567-e89b-12d3-a456-426614174000', - username: 'testuser', - email: 'test@example.com', - password: 'hashedPassword', - fname: 'Test', - lname: 'User', - orgid: null, - token: null, - TokenExpires: null, - phone: null, - state: UserState.REGISTERED_NOT_VERIFIED, - regdate: new Date('2025-01-01'), - updateDate: new Date('2025-01-01'), - Orglogindate: null, - get isAdmin() { return this.state === UserState.ADMIN; }, - ...overrides -}); - -export const createMockOrganization = (overrides: Partial = {}): OrganizationAggregate => ({ - id: '123e4567-e89b-12d3-a456-426614174001', - name: 'Test Organization', - contactfname: 'John', - contactlname: 'Doe', - contactphone: '+1234567890', - contactemail: 'contact@testorg.com', - state: OrganizationState.ACTIVE, - regdate: new Date('2025-01-01'), - updateDate: new Date('2025-01-01'), - url: null, - userinorg: 0, - maxOrganizationalDecks: 10, - users: [], - ...overrides -}); - -export const createMockDeck = (overrides: Partial = {}): DeckAggregate => ({ - id: '123e4567-e89b-12d3-a456-426614174002', - name: 'Test Deck', - type: DeckType.JOKER, - userid: '123e4567-e89b-12d3-a456-426614174000', - creationdate: new Date('2025-01-01'), - cards: [], - playedNumber: 0, - ctype: CType.PUBLIC, - updateDate: new Date('2025-01-01'), - state: DeckState.ACTIVE, - organization: null, - user: null, - isEditable: jest.fn().mockReturnValue(true), - ...overrides -}); - -export const createMockContact = (overrides: Partial = {}): ContactAggregate => ({ - id: '123e4567-e89b-12d3-a456-426614174003', - name: 'John Doe', - email: 'john.doe@example.com', - userid: '123e4567-e89b-12d3-a456-426614174000', - type: ContactType.QUESTION, - txt: 'This is a test contact message.', - state: ContactState.ACTIVE, - createDate: new Date('2025-01-01'), - updateDate: new Date('2025-01-01'), - adminResponse: null, - responseDate: null, - respondedBy: null, - ...overrides -}); - -export const createMockDate = () => new Date('2025-01-01T00:00:00Z'); - -// Mock Repository Factory Functions -export const createMockUserRepository = (): jest.Mocked => ({ - create: jest.fn(), - findByPage: jest.fn(), - findByPageIncludingDeleted: jest.fn(), - findById: jest.fn(), - findByIdIncludingDeleted: jest.fn(), - findByUsername: jest.fn(), - findByEmail: jest.fn(), - findByToken: jest.fn(), - search: jest.fn(), - searchIncludingDeleted: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - softDelete: jest.fn(), - deactivate: jest.fn(), - activate: jest.fn(), -} as jest.Mocked); - -export const createMockOrganizationRepository = (): jest.Mocked => ({ - create: jest.fn(), - findByPage: jest.fn(), - findByPageIncludingDeleted: jest.fn(), - findById: jest.fn(), - findByIdIncludingDeleted: jest.fn(), - search: jest.fn(), - searchIncludingDeleted: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - softDelete: jest.fn(), -} as jest.Mocked); - -export const createMockDeckRepository = (): jest.Mocked => ({ - create: jest.fn(), - findByPage: jest.fn(), - findByPageIncludingDeleted: jest.fn(), - findById: jest.fn(), - findByIdIncludingDeleted: jest.fn(), - search: jest.fn(), - searchIncludingDeleted: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - softDelete: jest.fn(), - countActiveByUserId: jest.fn(), - countOrganizationalByUserId: jest.fn(), - findFilteredDecks: jest.fn(), -} as jest.Mocked); - -export const createMockContactRepository = (): jest.Mocked => ({ - create: jest.fn(), - findById: jest.fn(), - findByPage: jest.fn(), - findByPageIncludingDeleted: jest.fn(), - findByIdIncludingDeleted: jest.fn(), - search: jest.fn(), - searchIncludingDeleted: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - softDelete: jest.fn(), -} as jest.Mocked); - -export const createMockJWTService = () => ({ - create: jest.fn(), - verify: jest.fn(), - shouldRefreshToken: jest.fn(), - parseDuration: jest.fn(), -} as any); - -export const createMockTokenService = () => ({ - generateSecureToken: jest.fn(), - generateVerificationToken: jest.fn(), - generatePasswordResetToken: jest.fn(), - isTokenExpired: jest.fn(), - validateToken: jest.fn(), -} as any); - -export const createMockEmailService = () => ({ - sendEmail: jest.fn(), - sendVerificationEmail: jest.fn(), - sendPasswordResetEmail: jest.fn(), - sendContactResponseEmail: jest.fn(), - loadTemplate: jest.fn(), -} as any); - -export const createMockPasswordService = () => ({ - hashPassword: jest.fn(), - verifyPassword: jest.fn(), - validatePasswordStrength: jest.fn(), - generateRandomPassword: jest.fn(), -} as any); diff --git a/SerpentRace_Backend/tsconfig.json b/SerpentRace_Backend/tsconfig.json deleted file mode 100644 index 3b8f8bca..00000000 --- a/SerpentRace_Backend/tsconfig.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "lib": ["ES2020"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "libReplacement": true, /* Enable lib replacement. */ - "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ - // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file diff --git a/SerpentRace_Docker/.env.dev b/SerpentRace_Docker/.env.dev deleted file mode 100644 index 5621c2a8..00000000 --- a/SerpentRace_Docker/.env.dev +++ /dev/null @@ -1,55 +0,0 @@ -# ============================================== -# SerpentRace Backend Environment Configuration -# ============================================== -# Copy this file to .env and fill in your values - -# APPLICATION CONFIGURATION -NODE_ENV=development -PORT=3000 -APP_BASE_URL=http://localhost:3000 -FRONTEND_URL=http://localhost:5173 - -# DATABASE CONFIGURATION (PostgreSQL) -DB_HOST=postgres -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=postgres - -# REDIS CONFIGURATION -REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_URL=redis://redis:6379 - -# MINIO CONFIGURATION -MINIO_ENDPOINT=minio -MINIO_PORT=9000 -MINIO_ACCESS_KEY=serpentrace -MINIO_SECRET_KEY=serpentrace123! -MINIO_USE_SSL=false -MINIO_BUCKET_NAME=serpentrace-logs - -# JWT CONFIGURATION -JWT_SECRET=your_super_secret_jwt_key_change_in_production -JWT_EXPIRY=86400 -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d -GAME_TOKEN_EXPIRY=86400 - -# EMAIL SERVICE CONFIGURATION -EMAIL_HOST=mail.serpentrace.hu -EMAIL_PORT=465 -EMAIL_SECURE=true -EMAIL_USER=noreply@serpentrace.hu -EMAIL_PASS=ZUx720ece&Cin&F{ -EMAIL_FROM=noreply@serpentrace.hu - -# CHAT SYSTEM CONFIGURATION -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 -CHAT_MAX_MESSAGES_PER_USER=100 -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# GAME CONFIGURATION -MAX_SPECIAL_FIELDS_PERCENTAGE=67 -MAX_GENERATION_TIME_SECONDS=20 -GENERATION_ERROR_TOLERANCE=15 diff --git a/SerpentRace_Docker/.env.example b/SerpentRace_Docker/.env.example deleted file mode 100644 index a1e8afbf..00000000 --- a/SerpentRace_Docker/.env.example +++ /dev/null @@ -1,222 +0,0 @@ -# ============================================== -# SerpentRace Backend Environment Configuration -# ============================================== -# Copy this file to .env and fill in your values -# This file contains all environment variables used by the backend - -# ============================================== -# APPLICATION CONFIGURATION -# ============================================== - -# Node.js environment (development, production, test) -NODE_ENV=development - -# Server port number -PORT=3000 - -# Base URL for the application (used for email links, etc.) -APP_BASE_URL=http://localhost:3000 - -# ============================================== -# DATABASE CONFIGURATION (PostgreSQL) -# ============================================== - -# Database connection details -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=your_db_password - -# Database URL (alternative to individual settings) -# DATABASE_URL=postgresql://username:password@localhost:5432/serpentrace - -# ============================================== -# REDIS CONFIGURATION -# ============================================== - -# Redis connection details (for caching and sessions) -REDIS_HOST=localhost -REDIS_PORT=6379 - -# Redis URL (alternative to individual settings) -REDIS_URL=redis://localhost:6379 - -# Redis password (if required) -# REDIS_PASSWORD=your_redis_password - -# ============================================== -# JWT (JSON Web Token) CONFIGURATION -# ============================================== - -# Secret key for JWT signing (REQUIRED - use a strong, random key in production) -JWT_SECRET=your_super_secret_jwt_key_change_in_production - -# JWT token expiration time -# Can be specified in seconds (e.g., 86400) or time format (e.g., 24h, 7d, 30m) -JWT_EXPIRY=86400 -# Alternative format -JWT_EXPIRATION=24h - -# JWT refresh token expiration (for future use) -JWT_REFRESH_EXPIRATION=7d - -# Game token expiration (for game session tokens) -GAME_TOKEN_EXPIRY=86400 - -# ============================================== -# EMAIL SERVICE CONFIGURATION (SMTP) -# ============================================== - -# SMTP server configuration -EMAIL_HOST=smtp.gmail.com -EMAIL_PORT=587 -EMAIL_SECURE=false - -# Email authentication -EMAIL_USER=your_email@domain.com -EMAIL_PASS=your_email_password - -# From address for outgoing emails -EMAIL_FROM=noreply@serpentrace.com - -# ============================================== -# CHAT SYSTEM CONFIGURATION -# ============================================== - -# Chat inactivity timeout (in minutes) -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 - -# Maximum messages per user per session -CHAT_MAX_MESSAGES_PER_USER=100 - -# Cleanup old messages after X weeks -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# ============================================== -# GAME CONFIGURATION -# ============================================== - -# Board generation settings -MAX_SPECIAL_FIELDS_PERCENTAGE=67 -MAX_GENERATION_TIME_SECONDS=20 -GENERATION_ERROR_TOLERANCE=15 - -# ============================================== -# MINIO/S3 CONFIGURATION (File Storage) -# ============================================== - -# MinIO server configuration (for file uploads) -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_ACCESS_KEY=serpentrace -MINIO_SECRET_KEY=serpentrace123! -MINIO_USE_SSL=false - -# S3 bucket name (if using S3 instead of MinIO) -# S3_BUCKET_NAME=serpentrace-files - -# ============================================== -# LOGGING CONFIGURATION -# ============================================== - -# Log level (error, warn, info, debug) -LOG_LEVEL=info - -# Log file retention (in days) -LOG_RETENTION_DAYS=30 - -# ============================================== -# SECURITY CONFIGURATION -# ============================================== - -# API rate limiting (requests per minute per IP) -RATE_LIMIT_RPM=60 - -# Maximum file upload size (in MB) -MAX_UPLOAD_SIZE_MB=10 - -# CORS allowed origins (comma-separated) -CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:8080 - -# ============================================== -# ADMIN CONFIGURATION -# ============================================== - -# Admin bypass settings -ADMIN_BYPASS_ENABLED=true - -# Default admin user (for development only) -# ADMIN_DEFAULT_EMAIL=admin@serpentrace.com -# ADMIN_DEFAULT_PASSWORD=change_this_password - -# ============================================== -# MONITORING & HEALTH CHECKS -# ============================================== - -# Health check endpoint timeout (in milliseconds) -HEALTH_CHECK_TIMEOUT=5000 - -# Database connection pool settings -DB_CONNECTION_POOL_MIN=2 -DB_CONNECTION_POOL_MAX=10 - -# ============================================== -# DEVELOPMENT ONLY SETTINGS -# ============================================== -# These settings should only be used in development - -# Enable detailed SQL logging -DB_LOGGING=true - -# Enable debug mode for various services -DEBUG_MODE=false - -# Disable email sending in development (logs emails instead) -EMAIL_DEBUG_MODE=true - -# ============================================== -# PRODUCTION ONLY SETTINGS -# ============================================== -# These settings are typically used only in production - -# Enable HTTPS (for production) -# HTTPS_ENABLED=true -# SSL_CERT_PATH=/path/to/cert.pem -# SSL_KEY_PATH=/path/to/key.pem - -# Sentry configuration (for error tracking) -# SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id - -# New Relic configuration (for performance monitoring) -# NEW_RELIC_LICENSE_KEY=your_new_relic_license_key -# NEW_RELIC_APP_NAME=SerpentRace Backend - -# ============================================== -# EXTERNAL API KEYS (Optional) -# ============================================== - -# Third-party service API keys (if used) -# ANALYTICS_API_KEY=your_analytics_key -# PAYMENT_API_KEY=your_payment_processor_key - -# ============================================== -# NOTES & SECURITY WARNINGS -# ============================================== - -# SECURITY NOTES: -# - Never commit .env files to version control -# - Use strong, unique passwords and keys -# - Regularly rotate JWT secrets and API keys -# - Use environment-specific values for each deployment - -# REQUIRED VARIABLES: -# The following variables are required for the application to start: -# - NODE_ENV -# - DB_HOST, DB_PORT, DB_NAME, DB_USERNAME, DB_PASSWORD -# - REDIS_HOST, REDIS_PORT -# - JWT_SECRET -# - EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS - -# OPTIONAL VARIABLES: -# All other variables have sensible defaults and are optional diff --git a/SerpentRace_Docker/.env.prod b/SerpentRace_Docker/.env.prod deleted file mode 100644 index 5b578f34..00000000 --- a/SerpentRace_Docker/.env.prod +++ /dev/null @@ -1,55 +0,0 @@ -# Production Environment Variables - -# Production settings -NODE_ENV=production - -#Backend -# Database -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=serpentrace_secure_password_2024! - -# PostgreSQL Database (for docker-compose) -POSTGRES_PASSWORD=serpentrace_secure_password_2024! - -# Redis -REDIS_URL=redis://localhost:6379 -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= - -# JWT - Use JWT_EXPIRY (seconds) or JWT_EXPIRATION (duration format like 24h, 7d) -JWT_SECRET=serpentrace_super_secure_jwt_secret_key_2024_production! -JWT_EXPIRY=86400 -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d - -# Email -EMAIL_HOST=smtp.example.com -EMAIL_PORT=587 -EMAIL_SECURE=false -EMAIL_USER=your_email@example.com -EMAIL_PASS=your_email_password -EMAIL_FROM="SerpentRace " - -# MinIO Object Storage -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_ACCESS_KEY=serpentrace_minio_admin -MINIO_SECRET_KEY=serpentrace_minio_secret_key_2024! -MINIO_BUCKET_NAME=serpentrace-logs - -# Application -APP_BASE_URL=http://localhost:3000 -PORT=3000 - -# Chat Limits -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 -CHAT_MAX_MESSAGES_PER_USER=100 -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# Logging -MAX_LOGS_PER_FILE=10000 diff --git a/SerpentRace_Docker/.env.server b/SerpentRace_Docker/.env.server deleted file mode 100644 index 5b578f34..00000000 --- a/SerpentRace_Docker/.env.server +++ /dev/null @@ -1,55 +0,0 @@ -# Production Environment Variables - -# Production settings -NODE_ENV=production - -#Backend -# Database -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -DB_PASSWORD=serpentrace_secure_password_2024! - -# PostgreSQL Database (for docker-compose) -POSTGRES_PASSWORD=serpentrace_secure_password_2024! - -# Redis -REDIS_URL=redis://localhost:6379 -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= - -# JWT - Use JWT_EXPIRY (seconds) or JWT_EXPIRATION (duration format like 24h, 7d) -JWT_SECRET=serpentrace_super_secure_jwt_secret_key_2024_production! -JWT_EXPIRY=86400 -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d - -# Email -EMAIL_HOST=smtp.example.com -EMAIL_PORT=587 -EMAIL_SECURE=false -EMAIL_USER=your_email@example.com -EMAIL_PASS=your_email_password -EMAIL_FROM="SerpentRace " - -# MinIO Object Storage -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_ACCESS_KEY=serpentrace_minio_admin -MINIO_SECRET_KEY=serpentrace_minio_secret_key_2024! -MINIO_BUCKET_NAME=serpentrace-logs - -# Application -APP_BASE_URL=http://localhost:3000 -PORT=3000 - -# Chat Limits -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 -CHAT_MAX_MESSAGES_PER_USER=100 -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# Logging -MAX_LOGS_PER_FILE=10000 diff --git a/SerpentRace_Docker/DOCKER_README.md b/SerpentRace_Docker/DOCKER_README.md deleted file mode 100644 index 9b004668..00000000 --- a/SerpentRace_Docker/DOCKER_README.md +++ /dev/null @@ -1,267 +0,0 @@ -# SerpentRace Docker Development Environment - -This Docker setup provides a complete development environment for SerpentRace with hot reloading and all necessary services. - -## 🚀 Quick Start - -### Development Environment - -1. **Start the development environment:** - ```bash - # Windows - docker-manage.bat dev:start - - # Linux/Mac - ./docker-manage.sh dev:start - ``` - -2. **Access the applications:** - - **Frontend:** http://localhost:5173 - - **Backend API:** http://localhost:3000 - - **Swagger API Docs:** http://localhost:3000/api-docs - - **PostgreSQL:** localhost:5432 (user: postgres, password: postgres) - - **Redis:** localhost:6379 - - **MinIO Console:** http://localhost:9001 (serpentrace / serpentrace123!) - - **PgAdmin:** http://localhost:8080 (admin@serpentrace.dev / admin) - - **Redis Commander:** http://localhost:8081 - -3. **Stop the environment:** - ```bash - # Windows - docker-manage.bat dev:stop - - # Linux/Mac - ./docker-manage.sh dev:stop - ``` - -### Production Environment - -1. **Configure production environment:** - - Copy `.env.prod` and update all values with secure passwords - - Update JWT secrets and database passwords - -2. **Start production:** - ```bash - # Windows - docker-manage.bat prod:start - - # Linux/Mac - ./docker-manage.sh prod:start - ``` - -## 📁 File Structure - -``` -SzeSnake/ -├── docker-compose.dev.yml # Development environment -├── docker-compose.prod.yml # Production environment -├── docker-manage.sh # Linux/Mac management script -├── docker-manage.bat # Windows management script -├── .env.dev # Development environment variables -├── .env.prod # Production environment variables -├── SerpentRace_Backend/ -│ ├── Dockerfile # Production backend image -│ ├── Dockerfile.dev # Development backend image -│ └── .dockerignore -└── SerpentRace_Frontend/ - ├── Dockerfile # Production frontend image - ├── Dockerfile.dev # Development frontend image - ├── nginx.conf # Nginx configuration for production - └── .dockerignore -``` - -## 🛠 Development Features - -### Hot Reloading -- **Backend:** Uses `nodemon` and `ts-node` for automatic TypeScript compilation and server restart -- **Frontend:** Uses Vite's built-in HMR (Hot Module Replacement) - -### Volume Mapping -- Source code is mounted as volumes for instant file changes -- Node modules are preserved in named volumes for performance - -### Development Tools -- **PgAdmin:** Web-based PostgreSQL administration -- **Redis Commander:** Web-based Redis management -- **MinIO Console:** Object storage management - -### Database Initialization -- Automatic database setup with test data from `sql_dump_with_test_data.sql` - -## 🐳 Docker Services - -### Backend (`backend`) -- **Image:** Node.js 20 Alpine -- **Port:** 3000 -- **Features:** Hot reload, TypeScript support -- **Dependencies:** PostgreSQL, Redis, MinIO - -### Frontend (`frontend`) -- **Image:** Node.js 20 Alpine (dev) / Nginx Alpine (prod) -- **Port:** 5173 (dev) / 80 (prod) -- **Features:** Vite HMR, React Fast Refresh - -### PostgreSQL (`postgres`) -- **Image:** PostgreSQL 15 Alpine -- **Port:** 5432 -- **Database:** serpentrace -- **Credentials:** postgres/postgres (dev) - -### Redis (`redis`) -- **Image:** Redis 7 Alpine -- **Port:** 6379 -- **Features:** Persistence enabled - -### MinIO (`minio`) -- **Image:** MinIO latest -- **Ports:** 9000 (API), 9001 (Console) -- **Features:** S3-compatible object storage - -## 🔧 Management Commands - -### Using the Management Scripts - -```bash -# Start development environment -./docker-manage.sh dev:start - -# Stop development environment -./docker-manage.sh dev:stop - -# View logs for all services -./docker-manage.sh logs - -# View logs for specific service -./docker-manage.sh logs backend -./docker-manage.sh logs frontend - -# Clean up all resources -./docker-manage.sh cleanup - -# Production commands -./docker-manage.sh prod:start -./docker-manage.sh prod:stop -``` - -### Manual Docker Compose Commands - -```bash -# Development -docker-compose -f docker-compose.dev.yml --env-file .env.dev up --build -d -docker-compose -f docker-compose.dev.yml down - -# Production -docker-compose -f docker-compose.prod.yml --env-file .env.prod up --build -d -docker-compose -f docker-compose.prod.yml down - -# View logs -docker-compose -f docker-compose.dev.yml logs -f [service_name] - -# Rebuild specific service -docker-compose -f docker-compose.dev.yml up --build backend - -# Execute commands in running containers -docker-compose -f docker-compose.dev.yml exec backend npm run test -docker-compose -f docker-compose.dev.yml exec postgres psql -U postgres -d serpentrace -``` - -## 🔒 Security Considerations - -### Development -- Default passwords are used for convenience -- Services are exposed on localhost -- Debug tools are included - -### Production -- **IMPORTANT:** Update all passwords in `.env.prod` -- Use strong JWT secrets (256+ characters recommended) -- Services are not directly exposed -- No debug tools included - -## 🐛 Troubleshooting - -### Common Issues - -1. **Port conflicts:** - - Check if ports 3000, 5173, 5432, 6379, 9000, 9001, 8080, 8081 are available - - Modify port mappings in docker-compose files if needed - -2. **File watching issues on Windows:** - - WSL2 is recommended for better file system performance - - Ensure Docker Desktop is configured to use WSL2 - -3. **Database connection issues:** - - Wait for health checks to pass before the application starts - - Check logs: `./docker-manage.sh logs postgres` - -4. **Hot reload not working:** - - Ensure volumes are properly mounted - - Check file permissions on Linux/Mac systems - -### Performance Tips - -1. **Use WSL2 on Windows** for better file system performance -2. **Increase Docker memory** allocation if needed -3. **Use .dockerignore** to exclude unnecessary files -4. **Prune unused Docker resources** regularly: `docker system prune` - -## 📝 Environment Variables - -### Development (.env.dev) -```bash -POSTGRES_PASSWORD=postgres -JWT_SECRET=dev_jwt_secret_change_in_production -MINIO_ACCESS_KEY=serpentrace -MINIO_SECRET_KEY=serpentrace123! -``` - -### Production (.env.prod) -```bash -POSTGRES_PASSWORD=your_secure_password -JWT_SECRET=your_very_long_secure_jwt_secret -MINIO_ACCESS_KEY=your_minio_access_key -MINIO_SECRET_KEY=your_secure_minio_secret -``` - -## 🔄 Health Checks - -All services include health checks to ensure proper startup order: -- **PostgreSQL:** `pg_isready` -- **Redis:** `redis-cli ping` -- **MinIO:** HTTP health endpoint -- **Backend:** HTTP health endpoint -- **Frontend:** HTTP health endpoint (production) - -The application will only start after all dependencies are healthy. - -## 📊 Monitoring - -### Logs -```bash -# All services -./docker-manage.sh logs - -# Specific service -./docker-manage.sh logs backend -./docker-manage.sh logs frontend -./docker-manage.sh logs postgres -``` - -### Service Status -```bash -# Check running containers -docker ps - -# Check service health -docker-compose -f docker-compose.dev.yml ps -``` - -## 🚀 Deployment - -For production deployment: - -1. Update `.env.prod` with secure values -2. Ensure proper firewall configuration -3. Use reverse proxy (nginx/traefik) for SSL termination -4. Consider using Docker Swarm or Kubernetes for orchestration -5. Set up monitoring and backup solutions diff --git a/SerpentRace_Docker/Dockerfile_backend b/SerpentRace_Docker/Dockerfile_backend deleted file mode 100644 index 236d55bf..00000000 --- a/SerpentRace_Docker/Dockerfile_backend +++ /dev/null @@ -1,60 +0,0 @@ -# Production Dockerfile for SerpentRace Backend -FROM node:20-alpine AS builder - -# Set working directory -WORKDIR /app - -# Install dependencies needed for native modules -RUN apk add --no-cache python3 make g++ - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install ALL dependencies for building (including devDependencies) -RUN npm ci - -# Copy source code -COPY . . - -# Build the application -RUN npm run build || echo "No build script found" - -# Production stage -FROM node:20-alpine AS production - -# Set working directory -WORKDIR /app - -# Install dependencies needed for native modules -RUN apk add --no-cache python3 make g++ - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install only production dependencies -RUN npm ci --only=production && npm cache clean --force - -# Copy built application from builder stage -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/package.json ./ - -# Create logs directory with proper permissions -RUN mkdir -p logs && chmod 777 logs - -# Create non-root user but don't switch to it for now -RUN addgroup -g 1001 -S nodejs -RUN adduser -S serpentrace -u 1001 -RUN chown -R serpentrace:nodejs /app - -# Keep running as root to avoid permission issues with mounted volumes -# USER serpentrace - -# Expose port -EXPOSE 3000 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:3000/health || exit 1 - -# Production command -CMD ["npm", "start"] diff --git a/SerpentRace_Docker/Dockerfile_backend.dev b/SerpentRace_Docker/Dockerfile_backend.dev deleted file mode 100644 index 0f977a19..00000000 --- a/SerpentRace_Docker/Dockerfile_backend.dev +++ /dev/null @@ -1,29 +0,0 @@ -# Development Dockerfile for SerpentRace Backend -FROM node:20-alpine - -# Set working directory -WORKDIR /app - -# Install dependencies needed for native modules -RUN apk add --no-cache python3 make g++ - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install dependencies -RUN npm install - -# Install nodemon globally for development -RUN npm install -g nodemon ts-node - -# Copy source code -COPY . . - -# Create logs directory -RUN mkdir -p logs - -# Expose port -EXPOSE 3000 - -# Development command with hot reload -CMD ["npm", "run", "dev"] diff --git a/SerpentRace_Docker/Dockerfile_frontend b/SerpentRace_Docker/Dockerfile_frontend deleted file mode 100644 index a2a6fb87..00000000 --- a/SerpentRace_Docker/Dockerfile_frontend +++ /dev/null @@ -1,36 +0,0 @@ -# Production Dockerfile for SerpentRace Frontend -FROM node:20-alpine AS builder - -# Set working directory -WORKDIR /app - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install dependencies (including devDependencies needed for build) -RUN npm ci - -# Copy source code -COPY . . - -# Build the application -RUN npm run build - -# Production stage with nginx -FROM nginx:alpine AS production - -# Copy built application -COPY --from=builder /app/dist /usr/share/nginx/html - -# Copy nginx configuration -COPY nginx.conf /etc/nginx/conf.d/default.conf - -# Expose port -EXPOSE 80 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost || exit 1 - -# Start nginx -CMD ["nginx", "-g", "daemon off;"] diff --git a/SerpentRace_Docker/Dockerfile_frontend.dev b/SerpentRace_Docker/Dockerfile_frontend.dev deleted file mode 100644 index c05e37de..00000000 --- a/SerpentRace_Docker/Dockerfile_frontend.dev +++ /dev/null @@ -1,20 +0,0 @@ -# Development Dockerfile for SerpentRace Frontend -FROM node:20-alpine - -# Set working directory -WORKDIR /app - -# Copy package files -COPY package.json package-lock.json* ./ - -# Install dependencies -RUN npm install - -# Copy source code -COPY . . - -# Expose port -EXPOSE 5173 - -# Development command with hot reload -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/SerpentRace_Docker/docker-compose.deploy.yml b/SerpentRace_Docker/docker-compose.deploy.yml deleted file mode 100644 index 0e2e6af9..00000000 --- a/SerpentRace_Docker/docker-compose.deploy.yml +++ /dev/null @@ -1,188 +0,0 @@ -version: '3.8' - -services: - # Backend service using pre-built image - backend: - image: serpentrace-backend:latest - container_name: serpentrace-backend - restart: unless-stopped - ports: - - "3000:3000" - environment: - - NODE_ENV=production - - PORT=3000 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=${POSTGRES_PASSWORD} - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - JWT_SECRET=${JWT_SECRET} - - JWT_EXPIRATION=${JWT_EXPIRATION:-24h} - - JWT_REFRESH_EXPIRATION=${JWT_REFRESH_EXPIRATION:-7d} - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} - - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} - - MINIO_USE_SSL=false - - EMAIL_HOST=${EMAIL_HOST} - - EMAIL_PORT=${EMAIL_PORT} - - EMAIL_SECURE=${EMAIL_SECURE} - - EMAIL_USER=${EMAIL_USER} - - EMAIL_PASS=${EMAIL_PASS} - - EMAIL_FROM=${EMAIL_FROM} - volumes: - - backend_logs:/app/logs - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service using pre-built image - frontend: - image: serpentrace-frontend:latest - container_name: serpentrace-frontend - restart: unless-stopped - ports: - - "80:80" - - "443:443" - depends_on: - - backend - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/health"] - interval: 30s - timeout: 10s - retries: 3 - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql - networks: - - serpentrace-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_data:/data - command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio - restart: unless-stopped - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} - MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} - volumes: - - minio_data:/data - command: server /data --console-address ":9001" - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - - -# Redis Commander for internal administration - redis-commander: - image: rediscommander/redis-commander:latest - container_name: serpentrace-redis-commander-dev - restart: unless-stopped - ports: - - "8081:8081" - environment: - - REDIS_HOSTS=local:redis:6379 - depends_on: - redis: - condition: service_healthy - networks: - - serpentrace-network - - # Database administration tool for internal administration - pgadmin: - image: dpage/pgadmin4:latest - container_name: serpentrace-pgadmin - restart: unless-stopped - ports: - - "5050:80" - environment: - PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev - PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_CONFIG_SERVER_MODE: 'False' - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False' - volumes: - - pgadmin_data:/var/lib/pgadmin - - ./deployment/pgadmin_servers_deployment.json:/pgadmin4/servers.json:ro - depends_on: - postgres: - condition: service_healthy - networks: - - serpentrace-network - - -volumes: - postgres_data: - driver: local - redis_data: - driver: local - minio_data: - driver: local - backend_logs: - driver: local - pgadmin_data: - driver: local - -networks: - serpentrace-network: - driver: bridge \ No newline at end of file diff --git a/SerpentRace_Docker/docker-compose.dev.yml b/SerpentRace_Docker/docker-compose.dev.yml deleted file mode 100644 index bb5ac37c..00000000 --- a/SerpentRace_Docker/docker-compose.dev.yml +++ /dev/null @@ -1,184 +0,0 @@ -version: '3.8' - -services: - # Backend service with hot reload - backend: - build: - context: ../SerpentRace_Backend - dockerfile: ../SerpentRace_Docker/Dockerfile_backend.dev - container_name: serpentrace-backend-dev - restart: unless-stopped - ports: - - "3000:3000" - environment: - - NODE_ENV=development - - PORT=3000 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - JWT_SECRET=dev_jwt_secret_change_in_production - - JWT_EXPIRATION=24h - - JWT_REFRESH_EXPIRATION=7d - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=serpentrace - - MINIO_SECRET_KEY=serpentrace123! - - MINIO_USE_SSL=false - volumes: - - ../SerpentRace_Backend:/app - - /app/node_modules - - ../SerpentRace_Backend/logs:/app/logs - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service with hot reload - frontend: - build: - context: ../SerpentRace_Frontend - dockerfile: ../SerpentRace_Docker/Dockerfile_frontend.dev - container_name: serpentrace-frontend-dev - restart: unless-stopped - ports: - - "5173:5173" - environment: - - NODE_ENV=development - - API_URL=http://localhost:3000 - volumes: - - ../SerpentRace_Frontend:/app - - /app/node_modules - depends_on: - - backend - networks: - - serpentrace-network - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres-dev - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_dev_data:/var/lib/postgresql/data - - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro - networks: - - serpentrace-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis-dev - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_dev_data:/data - command: redis-server --appendonly yes - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio-dev - restart: unless-stopped - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: serpentrace - MINIO_ROOT_PASSWORD: serpentrace123! - volumes: - - minio_dev_data:/data - command: server /data --console-address ":9001" - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Commander for development debugging - redis-commander: - image: rediscommander/redis-commander:latest - container_name: serpentrace-redis-commander-dev - restart: unless-stopped - ports: - - "8081:8081" - environment: - - REDIS_HOSTS=local:redis:6379 - depends_on: - redis: - condition: service_healthy - networks: - - serpentrace-network - - # Database administration tool - pgadmin: - image: dpage/pgadmin4:latest - container_name: serpentrace-pgadmin-dev - restart: unless-stopped - ports: - - "8080:80" - environment: - PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev - PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_CONFIG_SERVER_MODE: 'False' - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False' - volumes: - - pgadmin_dev_data:/var/lib/pgadmin - - ./pgadmin_servers.json:/pgadmin4/servers.json:ro - depends_on: - postgres: - condition: service_healthy - networks: - - serpentrace-network - -volumes: - postgres_dev_data: - driver: local - redis_dev_data: - driver: local - minio_dev_data: - driver: local - pgadmin_dev_data: - driver: local - -networks: - serpentrace-network: - driver: bridge diff --git a/SerpentRace_Docker/docker-compose.prod.yml b/SerpentRace_Docker/docker-compose.prod.yml deleted file mode 100644 index d6d9483c..00000000 --- a/SerpentRace_Docker/docker-compose.prod.yml +++ /dev/null @@ -1,161 +0,0 @@ -version: '3.8' - -services: - # Backend service - backend: - build: - context: ../SerpentRace_Backend - dockerfile: ../SerpentRace_Docker/Dockerfile_backend - container_name: serpentrace-backend - restart: unless-stopped - env_file: - - .env.prod - ports: - - "3000:3000" - environment: - - NODE_ENV=production - - PORT=3000 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=${DB_PASSWORD} - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - JWT_SECRET=${JWT_SECRET} - - JWT_EXPIRATION=${JWT_EXPIRATION:-24h} - - JWT_REFRESH_EXPIRATION=${JWT_REFRESH_EXPIRATION:-7d} - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} - - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} - - MINIO_USE_SSL=false - - MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME:-serpentrace-logs} - - EMAIL_HOST=${EMAIL_HOST} - - EMAIL_PORT=${EMAIL_PORT} - - EMAIL_SECURE=${EMAIL_SECURE} - - EMAIL_USER=${EMAIL_USER} - - EMAIL_PASS=${EMAIL_PASS} - - EMAIL_FROM=${EMAIL_FROM} - - APP_BASE_URL=${APP_BASE_URL:-http://localhost:3000} - - CHAT_INACTIVITY_TIMEOUT_MINUTES=${CHAT_INACTIVITY_TIMEOUT_MINUTES:-30} - - CHAT_MAX_MESSAGES_PER_USER=${CHAT_MAX_MESSAGES_PER_USER:-100} - - CHAT_MESSAGE_CLEANUP_WEEKS=${CHAT_MESSAGE_CLEANUP_WEEKS:-4} - - MAX_LOGS_PER_FILE=${MAX_LOGS_PER_FILE:-10000} - volumes: - - logs-data:/app/logs - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service with nginx - frontend: - build: - context: ../SerpentRace_Frontend - dockerfile: ../SerpentRace_Docker/Dockerfile_frontend - container_name: serpentrace-frontend - restart: unless-stopped - ports: - - "80:80" - depends_on: - - backend - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/health"] - interval: 30s - timeout: 10s - retries: 3 - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres - restart: unless-stopped - env_file: - - .env.prod - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_data:/var/lib/postgresql/data - networks: - - serpentrace-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_data:/data - command: redis-server --appendonly yes - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio - restart: unless-stopped - env_file: - - .env.prod - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} - MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} - volumes: - - minio_data:/data - command: server /data --console-address ":9001" - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - -volumes: - postgres_data: - driver: local - redis_data: - driver: local - minio_data: - driver: local - logs-data: - driver: local - -networks: - serpentrace-network: - driver: bridge diff --git a/SerpentRace_Docker/docker-compose.watch.nat.yml b/SerpentRace_Docker/docker-compose.watch.nat.yml deleted file mode 100644 index 6b13f4de..00000000 --- a/SerpentRace_Docker/docker-compose.watch.nat.yml +++ /dev/null @@ -1,206 +0,0 @@ -services: - # Backend service with hot reload - backend: - build: - context: ../SerpentRace_Backend - dockerfile: ../SerpentRace_Docker/Dockerfile_backend.dev - container_name: serpentrace-backend-dev - restart: unless-stopped - env_file: - - .env.dev - ports: - - "3000:3000" - environment: - - NODE_ENV=development - - PORT=3000 - - FRONTEND_URL=http://localhost:5173 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=serpentrace - - MINIO_SECRET_KEY=serpentrace123! - - MINIO_USE_SSL=false - volumes: [ ../SerpentRace_Backend/logs:/app/logs ] - develop: - watch: - - action: sync - path: ../SerpentRace_Backend/src - target: /app/src - ignore: - - node_modules/ - - dist/ - - "*.log" - - action: sync - path: ../SerpentRace_Backend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Backend/package-lock.json - - action: rebuild - path: ../SerpentRace_Backend/tsconfig.json - - action: rebuild - path: ./Dockerfile_backend.dev - - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - network_mode: host - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service with hot reload - frontend: - build: - context: ../SerpentRace_Frontend - dockerfile: ../SerpentRace_Docker/Dockerfile_frontend.dev - container_name: serpentrace-frontend-dev - restart: unless-stopped - ports: - - "5173:5173" - environment: - - NODE_ENV=development - - VITE_API_URL=http://localhost:3000 - volumes: [] - develop: - watch: - - action: sync - path: ../SerpentRace_Frontend/src - target: /app/src - ignore: - - node_modules/ - - dist/ - - "*.log" - - action: sync - path: ../SerpentRace_Frontend/public - target: /app/public - - action: sync - path: ../SerpentRace_Frontend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Frontend/package-lock.json - - action: rebuild - path: ../SerpentRace_Frontend/vite.config.js - - action: rebuild - path: ./Dockerfile_frontend.dev - depends_on: - - backend - network_mode: host - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres-dev - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_dev_data:/var/lib/postgresql/data - - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro - network_mode: host - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis-dev - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_dev_data:/data - command: redis-server --appendonly yes - network_mode: host - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio-dev - restart: unless-stopped - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: serpentrace - MINIO_ROOT_PASSWORD: serpentrace123! - volumes: - - minio_dev_data:/data - command: server /data --console-address ":9001" - network_mode: host - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Commander for development debugging - redis-commander: - image: rediscommander/redis-commander:latest - container_name: serpentrace-redis-commander-dev - restart: unless-stopped - ports: - - "8081:8081" - environment: - - REDIS_HOSTS=local:redis:6379 - depends_on: - redis: - condition: service_healthy - network_mode: host - - # Database administration tool - pgadmin: - image: dpage/pgadmin4:latest - container_name: serpentrace-pgadmin-dev - restart: unless-stopped - ports: - - "8080:80" - environment: - PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev - PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_CONFIG_SERVER_MODE: 'False' - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False' - volumes: - - pgadmin_dev_data:/var/lib/pgadmin - - ./pgadmin_servers.json:/pgadmin4/servers.json:ro - depends_on: - postgres: - condition: service_healthy - network_mode: host - -volumes: - postgres_dev_data: - driver: local - redis_dev_data: - driver: local - minio_dev_data: - driver: local - pgadmin_dev_data: - driver: local diff --git a/SerpentRace_Docker/docker-compose.watch.yml b/SerpentRace_Docker/docker-compose.watch.yml deleted file mode 100644 index 949a4770..00000000 --- a/SerpentRace_Docker/docker-compose.watch.yml +++ /dev/null @@ -1,217 +0,0 @@ -services: - # Backend service with hot reload - backend: - build: - context: ../SerpentRace_Backend - dockerfile: ../SerpentRace_Docker/Dockerfile_backend.dev - container_name: serpentrace-backend-dev - restart: unless-stopped - env_file: - - .env.dev - ports: - - "3000:3000" - environment: - - NODE_ENV=development - - PORT=3000 - - DB_HOST=postgres - - DB_PORT=5432 - - DB_NAME=serpentrace - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - REDIS_URL=redis://redis:6379 - - REDIS_HOST=redis - - REDIS_PORT=6379 - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=serpentrace - - MINIO_SECRET_KEY=serpentrace123! - - MINIO_USE_SSL=false - volumes: [ ../SerpentRace_Backend/logs:/app/logs ] - develop: - watch: - - action: sync - path: ../SerpentRace_Backend/src - target: /app/src - ignore: - - node_modules/ - - dist/ - - "*.log" - - action: sync - path: ../SerpentRace_Backend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Backend/package-lock.json - - action: rebuild - path: ../SerpentRace_Backend/tsconfig.json - - action: rebuild - path: ./Dockerfile_backend.dev - - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - minio: - condition: service_healthy - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend service with hot reload - frontend: - build: - context: ../SerpentRace_Frontend - dockerfile: ../SerpentRace_Docker/Dockerfile_frontend.dev - container_name: serpentrace-frontend-dev - restart: unless-stopped - ports: - - "5173:5173" - environment: - - NODE_ENV=development - - VITE_API_URL=http://localhost:3000 - volumes: - [] - develop: - watch: - - action: sync - path: ../SerpentRace_Frontend/src - target: /app/src - ignore: - - node_modules/ - - dist/ - - "*.log" - - action: sync - path: ../SerpentRace_Frontend/public - target: /app/public - - action: sync - path: ../SerpentRace_Frontend/package.json - target: /app/package.json - - action: rebuild - path: ../SerpentRace_Frontend/package-lock.json - - action: rebuild - path: ../SerpentRace_Frontend/vite.config.js - - action: rebuild - path: ./Dockerfile_frontend.dev - depends_on: - - backend - networks: - - serpentrace-network - - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: serpentrace-postgres-dev - restart: unless-stopped - ports: - - "5432:5432" - environment: - POSTGRES_DB: serpentrace - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: "--encoding=UTF-8" - volumes: - - postgres_dev_data:/var/lib/postgresql/data - - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro - networks: - - serpentrace-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: serpentrace-redis-dev - restart: unless-stopped - ports: - - "6379:6379" - volumes: - - redis_dev_data:/data - command: redis-server --appendonly yes - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: serpentrace-minio-dev - restart: unless-stopped - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: serpentrace - MINIO_ROOT_PASSWORD: serpentrace123! - volumes: - - minio_dev_data:/data - command: server /data --console-address ":9001" - networks: - - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Commander for development debugging - redis-commander: - image: rediscommander/redis-commander:latest - container_name: serpentrace-redis-commander-dev - restart: unless-stopped - ports: - - "8081:8081" - environment: - - REDIS_HOSTS=local:redis:6379 - depends_on: - redis: - condition: service_healthy - networks: - - serpentrace-network - - # Database administration tool - pgadmin: - image: dpage/pgadmin4:latest - container_name: serpentrace-pgadmin-dev - restart: unless-stopped - ports: - - "8080:80" - environment: - PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev - PGADMIN_DEFAULT_PASSWORD: admin - PGADMIN_CONFIG_SERVER_MODE: 'False' - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False' - PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False' - volumes: - - pgadmin_dev_data:/var/lib/pgadmin - - ./pgadmin_servers.json:/pgadmin4/servers.json:ro - depends_on: - postgres: - condition: service_healthy - networks: - - serpentrace-network - -volumes: - postgres_dev_data: - driver: local - redis_dev_data: - driver: local - minio_dev_data: - driver: local - pgadmin_dev_data: - driver: local - -networks: - serpentrace-network: - driver: bridge diff --git a/SerpentRace_Docker/docker-manage.bat b/SerpentRace_Docker/docker-manage.bat deleted file mode 100644 index 856d6a97..00000000 --- a/SerpentRace_Docker/docker-manage.bat +++ /dev/null @@ -1,57 +0,0 @@ -```bat -@echo off -setlocal - -rem Define your services here -set SERVICES= - -rem Define the environment file -set ENV_FILE=.env - -rem Load the environment variables -if exist "%ENV_FILE%" ( - for /f "usebackq tokens=*" %%i in ("%ENV_FILE%") do ( - set "%%i" - ) -) - -rem Define the default action -set ACTION=up - -rem Parse command line arguments -:parse_args -if "%~1"=="" goto :end_parse -if "%~1"=="--build" ( - set ACTION=build -) else if "%~1"=="--down" ( - set ACTION=down -) else if "%~1"=="--help" ( - goto :help -) else if "%~1"=="dev:watch" ( - goto :dev_watch -) -shift -goto :parse_args - -:end_parse - -rem Display help -:help -echo Usage: docker-compose-wrapper [options] -echo. -echo Options: -echo --build Build the services -echo --down Stop and remove the containers -echo --help Display this help message -echo dev:watch Start development environment with file watchers -goto :eof - -rem Development watch mode -:dev_watch -echo Starting development environment with file watchers... -docker-compose -f docker-compose.watch.yml up --build -goto :eof - -rem Execute the docker-compose command with the parsed action -%DOCKER_COMPOSE% %ACTION% %SERVICES% -``` \ No newline at end of file diff --git a/SerpentRace_Docker/nginx.conf b/SerpentRace_Docker/nginx.conf deleted file mode 100644 index fa4630a8..00000000 --- a/SerpentRace_Docker/nginx.conf +++ /dev/null @@ -1,60 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html index.htm; - - # Enable gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - - # Handle client routing - location / { - try_files $uri $uri/ /index.html; - } - - # API proxy to backend - location /api/ { - proxy_pass http://backend:3000/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } - - # WebSocket support - location /socket.io/ { - proxy_pass http://backend:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # Static assets caching - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Health check endpoint - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - } -} diff --git a/SerpentRace_Docker/pgadmin_servers.json b/SerpentRace_Docker/pgadmin_servers.json deleted file mode 100644 index 828e872e..00000000 --- a/SerpentRace_Docker/pgadmin_servers.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "Servers": { - "1": { - "Name": "SerpentRace PostgreSQL Dev", - "Group": "Development", - "Host": "postgres", - "Port": 5432, - "MaintenanceDB": "serpentrace", - "Username": "postgres", - "UseSSLMode": "prefer", - "SSLMode": "prefer", - "SSLCompression": 0, - "Timeout": 10, - "UseSSHTunnel": 0, - "TunnelPort": "22", - "TunnelAuthentication": 0, - "KerberosAuthentication": false, - "ConnectionParameters": { - "sslmode": "prefer", - "connect_timeout": "10" - } - } - } -} diff --git a/SerpentRace_Docker/sql_dump_with_test_data.sql b/SerpentRace_Docker/sql_dump_with_test_data.sql deleted file mode 100644 index 7271b451..00000000 --- a/SerpentRace_Docker/sql_dump_with_test_data.sql +++ /dev/null @@ -1,369 +0,0 @@ --- SerpentRace Backend Database Schema and Test Data --- Generated on: August 22, 2025 --- PostgreSQL Database Dump - --- Enable UUID extension -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- ============================================================================ --- DROP EXISTING TABLES (in reverse dependency order) --- ============================================================================ -DROP TABLE IF EXISTS "ChatArchives"; -DROP TABLE IF EXISTS "Chats"; -DROP TABLE IF EXISTS "Contacts"; -DROP TABLE IF EXISTS "Decks"; -DROP TABLE IF EXISTS "Users"; -DROP TABLE IF EXISTS "Organizations"; -DROP TABLE IF EXISTS "migrations"; - --- ============================================================================ --- CREATE TABLES --- ============================================================================ - --- Organizations Table -CREATE TABLE "Organizations" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "name" character varying(255) NOT NULL, - "contactfname" character varying(100) NOT NULL, - "contactlname" character varying(100) NOT NULL, - "contactphone" character varying(20) NOT NULL, - "contactemail" character varying(255) NOT NULL, - "state" integer NOT NULL DEFAULT 0, - "regdate" TIMESTAMP NOT NULL DEFAULT now(), - "updatedate" TIMESTAMP NOT NULL DEFAULT now(), - "url" character varying(500), - "userinorg" integer NOT NULL DEFAULT 0, - "maxOrganizationalDecks" integer, - CONSTRAINT "PK_Organizations" PRIMARY KEY ("id") -); - --- Users Table -CREATE TABLE "Users" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "orgid" uuid, - "username" character varying(100) NOT NULL UNIQUE, - "password" character varying(255) NOT NULL, - "email" character varying(255) NOT NULL UNIQUE, - "fname" character varying(100) NOT NULL, - "lname" character varying(100) NOT NULL, - "token" character varying(255), - "TokenExpires" TIMESTAMP, - "phone" character varying(20), - "state" integer NOT NULL DEFAULT 0, - "regdate" TIMESTAMP NOT NULL DEFAULT now(), - "updatedate" TIMESTAMP NOT NULL DEFAULT now(), - "Orglogindate" TIMESTAMP, - CONSTRAINT "PK_Users" PRIMARY KEY ("id"), - CONSTRAINT "FK_Users_Organizations" FOREIGN KEY ("orgid") REFERENCES "Organizations"("id") -); - --- Decks Table -CREATE TABLE "Decks" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "name" character varying(255) NOT NULL, - "type" integer NOT NULL, - "user_id" uuid NOT NULL, - "creation_date" TIMESTAMP NOT NULL DEFAULT now(), - "cards" json NOT NULL, - "played_number" integer NOT NULL DEFAULT 0, - "ctype" integer NOT NULL DEFAULT 0, - "update_date" TIMESTAMP NOT NULL DEFAULT now(), - "state" integer NOT NULL DEFAULT 0, - "organization_id" uuid, - CONSTRAINT "PK_Decks" PRIMARY KEY ("id"), - CONSTRAINT "FK_Decks_Users" FOREIGN KEY ("user_id") REFERENCES "Users"("id"), - CONSTRAINT "FK_Decks_Organizations" FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") -); - --- Chats Table -CREATE TABLE "Chats" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "users" uuid[] NOT NULL, - "messages" json NOT NULL DEFAULT '[]', - "updateDate" TIMESTAMP NOT NULL DEFAULT now(), - "state" integer NOT NULL DEFAULT 0, - "type" character varying(50) NOT NULL DEFAULT 'direct', - "name" character varying(255), - "gameId" uuid, - "createdBy" uuid, - "lastActivity" TIMESTAMP, - "createDate" TIMESTAMP NOT NULL DEFAULT now(), - "archiveDate" TIMESTAMP, - CONSTRAINT "PK_Chats" PRIMARY KEY ("id") -); - --- Chat Archives Table -CREATE TABLE "ChatArchives" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "chatId" uuid NOT NULL, - "archivedMessages" json NOT NULL, - "archivedAt" TIMESTAMP NOT NULL, - "createDate" TIMESTAMP NOT NULL DEFAULT now(), - "chatType" character varying(50) NOT NULL, - "chatName" character varying(255), - "gameId" uuid, - "participants" uuid[] NOT NULL, - CONSTRAINT "PK_ChatArchives" PRIMARY KEY ("id") -); - --- Contacts Table -CREATE TABLE "Contacts" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "name" character varying(255) NOT NULL, - "email" character varying(255) NOT NULL, - "userid" uuid, - "type" integer NOT NULL, - "txt" text NOT NULL, - "state" integer NOT NULL DEFAULT 0, - "createDate" TIMESTAMP NOT NULL DEFAULT now(), - "updateDate" TIMESTAMP NOT NULL DEFAULT now(), - "adminResponse" text, - "responseDate" TIMESTAMP, - "respondedBy" uuid, - CONSTRAINT "PK_Contacts" PRIMARY KEY ("id"), - CONSTRAINT "FK_Contacts_Users" FOREIGN KEY ("userid") REFERENCES "Users"("id"), - CONSTRAINT "FK_Contacts_Admins" FOREIGN KEY ("respondedBy") REFERENCES "Users"("id") -); - --- Migrations table (for TypeORM) -CREATE TABLE "migrations" ( - "id" SERIAL NOT NULL, - "timestamp" bigint NOT NULL, - "name" character varying NOT NULL, - CONSTRAINT "PK_migrations" PRIMARY KEY ("id") -); - --- ============================================================================ --- CREATE INDEXES --- ============================================================================ -CREATE INDEX "IDX_DECK_USER_STATE_CTYPE" ON "Decks" ("user_id", "state", "ctype"); -CREATE INDEX "IDX_DECK_ORG_CTYPE_STATE" ON "Decks" ("organization_id", "ctype", "state"); -CREATE INDEX "IDX_USERS_EMAIL" ON "Users" ("email"); -CREATE INDEX "IDX_USERS_USERNAME" ON "Users" ("username"); -CREATE INDEX "IDX_USERS_ORGID" ON "Users" ("orgid"); - --- ============================================================================ --- INSERT TEST DATA --- ============================================================================ - --- Organizations Test Data -INSERT INTO "Organizations" ("id", "name", "contactfname", "contactlname", "contactphone", "contactemail", "state", "regdate", "updatedate", "url", "userinorg", "maxOrganizationalDecks") VALUES -('11111111-1111-1111-1111-111111111111', 'Tech Solutions Inc', 'John', 'Smith', '+1-555-0001', 'john.smith@techsolutions.com', 1, '2024-01-15 10:00:00', '2024-01-15 10:00:00', 'https://techsolutions.com', 5, 20), -('22222222-2222-2222-2222-222222222222', 'Educational Institute', 'Sarah', 'Johnson', '+1-555-0002', 'sarah.johnson@eduinst.edu', 1, '2024-02-01 09:30:00', '2024-02-01 09:30:00', 'https://eduinstitute.edu', 15, 50), -('33333333-3333-3333-3333-333333333333', 'Healthcare Corp', 'Michael', 'Brown', '+1-555-0003', 'michael.brown@healthcorp.com', 0, '2024-03-10 14:20:00', '2024-03-10 14:20:00', NULL, 0, 10); - --- Users Test Data -INSERT INTO "Users" ("id", "orgid", "username", "password", "email", "fname", "lname", "token", "TokenExpires", "phone", "state", "regdate", "updatedate", "Orglogindate") VALUES --- Regular users -('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', NULL, 'john_doe', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'john.doe@email.com', 'John', 'Doe', NULL, NULL, '+1-555-1001', 1, '2024-01-20 11:00:00', '2024-01-20 11:00:00', NULL), -('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111', 'jane_premium', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'jane.smith@email.com', 'Jane', 'Smith', NULL, NULL, '+1-555-1002', 2, '2024-01-25 12:30:00', '2024-01-25 12:30:00', '2024-01-25 12:30:00'), -('cccccccc-cccc-cccc-cccc-cccccccccccc', '22222222-2222-2222-2222-222222222222', 'teacher_bob', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'bob.teacher@eduinst.edu', 'Bob', 'Teacher', NULL, NULL, '+1-555-1003', 2, '2024-02-05 09:15:00', '2024-02-05 09:15:00', '2024-02-05 09:15:00'), --- Admin user -('dddddddd-dddd-dddd-dddd-dddddddddddd', NULL, 'admin_user', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'admin@serpentrace.com', 'Admin', 'User', NULL, NULL, 'admin', '+1-555-9999', 5, '2024-01-01 08:00:00', '2024-01-01 08:00:00', NULL), --- Unverified user -('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', NULL, 'new_user', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'newuser@email.com', 'New', 'User', 'verification_token_12345', '2025-08-23 23:59:59', 'personal', NULL, 0, '2025-08-22 16:00:00', '2025-08-22 16:00:00', NULL); - --- Decks Test Data -INSERT INTO "Decks" ("id", "name", "type", "user_id", "creation_date", "cards", "played_number", "ctype", "update_date", "state", "organization_id") VALUES --- Public decks -('dddd1111-1111-1111-1111-111111111111', 'General Knowledge Quiz', 2, 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '2024-02-01 10:00:00', -'[ - {"id": "c1", "type": 0, "text": "What is the capital of France?", "answer": "Paris", "options": ["London", "Paris", "Berlin", "Madrid"]}, - {"id": "c2", "type": 0, "text": "Which planet is known as the Red Planet?", "answer": "Mars", "options": ["Venus", "Mars", "Jupiter", "Saturn"]}, - {"id": "c3", "type": 1, "text": "The Great Wall of China", "answer": "is visible from space", "options": ["is visible from space", "was built in one century"]}, - {"id": "c4", "type": 2, "text": "Describe the process of photosynthesis", "answer": null}, - {"id": "c5", "type": 3, "text": "The Earth is flat", "answer": false} -]', -25, 0, '2024-02-01 10:00:00', 0, NULL), - -('dddd2222-2222-2222-2222-222222222222', 'Math Fundamentals', 2, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '2024-02-05 14:30:00', -'[ - {"id": "m1", "type": 0, "text": "What is 2 + 2?", "answer": "4", "options": ["3", "4", "5", "6"]}, - {"id": "m2", "type": 0, "text": "What is the square root of 16?", "answer": "4", "options": ["2", "4", "8", "16"]}, - {"id": "m3", "type": 3, "text": "Pi is approximately 3.14", "answer": true}, - {"id": "m4", "type": 4, "text": "Complete the sequence: 2, 4, 6, ?", "answer": "8"} -]', -15, 0, '2024-02-05 14:30:00', 0, NULL), - --- Private decks -('dddd3333-3333-3333-3333-333333333333', 'My Personal Study Notes', 2, 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '2024-02-10 16:45:00', -'[ - {"id": "p1", "type": 2, "text": "What did I learn about React hooks today?", "answer": null}, - {"id": "p2", "type": 2, "text": "Key points from the management meeting", "answer": null} -]', -3, 1, '2024-02-10 16:45:00', 0, NULL), - --- Organizational decks -('dddd4444-4444-4444-4444-444444444444', 'Company Training Module', 2, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '2024-02-15 11:20:00', -'[ - {"id": "o1", "type": 0, "text": "What is our company policy on remote work?", "answer": "Flexible hybrid model", "options": ["No remote work", "Full remote", "Flexible hybrid model", "Weekends only"]}, - {"id": "o2", "type": 3, "text": "All employees must attend the monthly all-hands meeting", "answer": true}, - {"id": "o3", "type": 2, "text": "Describe the steps for requesting vacation time", "answer": null} -]', -8, 2, '2024-02-15 11:20:00', 0, '11111111-1111-1111-1111-111111111111'), - -('dddd5555-5555-5555-5555-555555555555', 'Educational Content for Students', 2, 'cccccccc-cccc-cccc-cccc-cccccccccccc', '2024-03-01 08:15:00', -'[ - {"id": "e1", "type": 0, "text": "When did World War II end?", "answer": "1945", "options": ["1943", "1944", "1945", "1946"]}, - {"id": "e2", "type": 1, "text": "Shakespeare wrote", "answer": "Romeo and Juliet", "options": ["Romeo and Juliet", "The Great Gatsby"]}, - {"id": "e3", "type": 3, "text": "The American Revolution began in 1776", "answer": false}, - {"id": "e4", "type": 4, "text": "Name three primary colors", "answer": "Red, Blue, Yellow"} -]', -42, 2, '2024-03-01 08:15:00', 0, '22222222-2222-2222-2222-222222222222'), - --- Joker and Luck type decks -('dddd6666-6666-6666-6666-666666666666', 'Lucky Challenges', 0, 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '2024-03-05 13:40:00', -'[ - {"id": "l1", "type": 4, "text": "Do 10 jumping jacks", "answer": null}, - {"id": "l2", "type": 4, "text": "Name your favorite childhood memory", "answer": null}, - {"id": "l3", "type": 4, "text": "Sing happy birthday", "answer": null} -]', -7, 0, '2024-03-05 13:40:00', 0, NULL), - -('dddd7777-7777-7777-7777-777777777777', 'Wild Cards', 1, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '2024-03-08 19:25:00', -'[ - {"id": "j1", "type": 4, "text": "Skip your next turn", "answer": null}, - {"id": "j2", "type": 4, "text": "Draw two extra cards", "answer": null}, - {"id": "j3", "type": 4, "text": "Trade places with another player", "answer": null}, - {"id": "j4", "type": 4, "text": "Double your next score", "answer": null} -]', -12, 0, '2024-03-08 19:25:00', 0, NULL); - --- Chats Test Data -INSERT INTO "Chats" ("id", "users", "messages", "updateDate", "state", "type", "name", "gameId", "createdBy", "lastActivity", "createDate", "archiveDate") VALUES --- Direct message between two users -('chat1111-1111-1111-1111-111111111111', -'{"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}', -'[ - {"id": "msg1", "date": "2024-03-20T10:30:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Hey Jane! How are you doing?"}, - {"id": "msg2", "date": "2024-03-20T10:32:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Hi John! I'\''m great, thanks for asking. How about you?"}, - {"id": "msg3", "date": "2024-03-20T10:35:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Doing well! Want to play a quiz game later?"}, - {"id": "msg4", "date": "2024-03-20T10:37:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Absolutely! I'\''ll prepare some questions."} -]', -'2024-03-20 10:37:00', 0, 'direct', NULL, NULL, NULL, '2024-03-20 10:37:00', '2024-03-20 10:30:00', NULL), - --- Group chat for organization -('chat2222-2222-2222-2222-222222222222', -'{"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "cccccccc-cccc-cccc-cccc-cccccccccccc", "dddddddd-dddd-dddd-dddd-dddddddddddd"}', -'[ - {"id": "msg5", "date": "2024-03-21T14:15:00Z", "userid": "dddddddd-dddd-dddd-dddd-dddddddddddd", "text": "Welcome everyone to the study group!"}, - {"id": "msg6", "date": "2024-03-21T14:16:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Thanks for organizing this!"}, - {"id": "msg7", "date": "2024-03-21T14:18:00Z", "userid": "cccccccc-cccc-cccc-cccc-cccccccccccc", "text": "I'\''ve prepared some educational content to share"}, - {"id": "msg8", "date": "2024-03-21T14:20:00Z", "userid": "dddddddd-dddd-dddd-dddd-dddddddddddd", "text": "Great! Let'\''s start with the basics"} -]', -'2024-03-21 14:20:00', 0, 'group', 'Study Group', NULL, 'dddddddd-dddd-dddd-dddd-dddddddddddd', '2024-03-21 14:20:00', '2024-03-21 14:15:00', NULL), - --- Game chat -('chat3333-3333-3333-3333-333333333333', -'{"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}', -'[ - {"id": "msg9", "date": "2024-03-22T16:45:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Ready to start the quiz game?"}, - {"id": "msg10", "date": "2024-03-22T16:46:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Yes! Let'\''s do this!"}, - {"id": "msg11", "date": "2024-03-22T16:50:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Great job on that last question!"}, - {"id": "msg12", "date": "2024-03-22T16:52:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Thanks! This is fun!"} -]', -'2024-03-22 16:52:00', 0, 'game', 'Quiz Game Session', 'game1111-1111-1111-1111-111111111111', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '2024-03-22 16:52:00', '2024-03-22 16:45:00', NULL); - --- Chat Archives Test Data -INSERT INTO "ChatArchives" ("id", "chatId", "archivedMessages", "archivedAt", "createDate", "chatType", "chatName", "gameId", "participants") VALUES -('arch1111-1111-1111-1111-111111111111', 'chat0000-0000-0000-0000-000000000000', -'[ - {"id": "oldmsg1", "date": "2024-01-15T09:00:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "This is an old conversation"}, - {"id": "oldmsg2", "date": "2024-01-15T09:05:00Z", "userid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "text": "Yes, from last month"}, - {"id": "oldmsg3", "date": "2024-01-15T09:10:00Z", "userid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "text": "Good times!"} -]', -'2024-02-15 00:00:00', '2024-02-15 00:00:00', 'direct', NULL, NULL, '{"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}'); - --- Contacts Test Data -INSERT INTO "Contacts" ("id", "name", "email", "userid", "type", "txt", "state", "createDate", "updateDate", "adminResponse", "responseDate", "respondedBy") VALUES --- Bug report from registered user -('cont1111-1111-1111-1111-111111111111', 'John Doe', 'john.doe@email.com', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 0, 'I found a bug when creating a new deck. The cards are not saving properly when I add more than 10 cards.', 1, '2024-03-18 14:30:00', '2024-03-19 09:15:00', 'Thank you for reporting this issue. We have identified the problem and deployed a fix. Please try creating your deck again.', '2024-03-19 09:15:00', 'dddddddd-dddd-dddd-dddd-dddddddddddd'), - --- General question from anonymous user -('cont2222-2222-2222-2222-222222222222', 'Sarah Wilson', 'sarah.wilson@email.com', NULL, 2, 'Hi, I'\''m interested in using SerpentRace for my classroom. Do you have any educational pricing or features specifically designed for teachers?', 0, '2024-03-19 11:20:00', '2024-03-19 11:20:00', NULL, NULL, NULL), - --- Problem report from premium user -('cont3333-3333-3333-3333-333333333333', 'Jane Smith', 'jane.smith@email.com', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 1, 'I'\''m having trouble with the organization deck sharing feature. When I share a deck with my team, they can'\''t see the latest updates I made.', 0, '2024-03-20 16:45:00', '2024-03-20 16:45:00', NULL, NULL, NULL), - --- Sales inquiry -('cont4444-4444-4444-4444-444444444444', 'Michael Chen', 'michael.chen@company.com', NULL, 3, 'Our company is interested in purchasing premium licenses for 50 employees. Could you provide pricing information and enterprise features?', 0, '2024-03-21 10:10:00', '2024-03-21 10:10:00', NULL, NULL, NULL), - --- Other type of contact -('cont5555-5555-5555-5555-555555555555', 'Lisa Johnson', 'lisa.johnson@email.com', NULL, 4, 'I love using SerpentRace! Could you add support for audio questions in the quiz decks? This would be great for language learning.', 0, '2024-03-22 13:25:00', '2024-03-22 13:25:00', NULL, NULL, NULL); - --- Migration entries -INSERT INTO "migrations" ("timestamp", "name") VALUES -(1755691733404, 'test1755691733404'), -(1755706019351, 'AddEmailVerificationFields1755706019351'), -(1755817306222, 'AddChatMessagingSystem1755817306222'), -(1755855028839, 'CreateContactTable1755855028839'), -(1692712800000, 'AddMaxOrganizationalDecksToOrganization1692712800000'); - --- ============================================================================ --- UPDATE ORGANIZATION USER COUNTS --- ============================================================================ -UPDATE "Organizations" SET "userinorg" = ( - SELECT COUNT(*) FROM "Users" WHERE "Users"."orgid" = "Organizations"."id" -); - --- ============================================================================ --- HELPFUL QUERIES FOR TESTING --- ============================================================================ - --- Query to see all users with their organizations --- SELECT u.username, u.email, u.state, o.name as organization_name --- FROM "Users" u --- LEFT JOIN "Organizations" o ON u.orgid = o.id; - --- Query to see deck distribution by type and visibility --- SELECT --- CASE ctype --- WHEN 0 THEN 'Public' --- WHEN 1 THEN 'Private' --- WHEN 2 THEN 'Organization' --- END as deck_type, --- CASE type --- WHEN 0 THEN 'Luck' --- WHEN 1 THEN 'Joker' --- WHEN 2 THEN 'Question' --- END as card_type, --- COUNT(*) as count --- FROM "Decks" --- WHERE state = 0 --- GROUP BY ctype, type --- ORDER BY ctype, type; - --- Query to see active chats with participant count --- SELECT --- id, --- type, --- name, --- array_length(users, 1) as participant_count, --- json_array_length(messages) as message_count, --- lastActivity --- FROM "Chats" --- WHERE state = 0 --- ORDER BY lastActivity DESC; - --- Query to see contact distribution by type and status --- SELECT --- CASE type --- WHEN 0 THEN 'Bug' --- WHEN 1 THEN 'Problem' --- WHEN 2 THEN 'Question' --- WHEN 3 THEN 'Sales' --- WHEN 4 THEN 'Other' --- END as contact_type, --- CASE state --- WHEN 0 THEN 'Active' --- WHEN 1 THEN 'Resolved' --- WHEN 2 THEN 'Deleted' --- END as status, --- COUNT(*) as count --- FROM "Contacts" --- GROUP BY type, state --- ORDER BY type, state; - --- ============================================================================ --- END OF SQL DUMP --- ============================================================================ diff --git a/SerpentRace_Docker/sql_schema_only.sql b/SerpentRace_Docker/sql_schema_only.sql deleted file mode 100644 index a5d28f0e..00000000 --- a/SerpentRace_Docker/sql_schema_only.sql +++ /dev/null @@ -1,180 +0,0 @@ --- This script was generated by the ERD tool in pgAdmin 4. --- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps. -BEGIN; - --- =================================================================== --- STEP 1: Enable Required Extensions --- =================================================================== -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- =================================================================== --- STEP 2: Create Tables --- =================================================================== - -CREATE TABLE IF NOT EXISTS public."ChatArchives" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - "chatId" uuid NOT NULL, - "archivedMessages" json NOT NULL, - "archivedAt" timestamp without time zone NOT NULL, - "createDate" timestamp without time zone NOT NULL DEFAULT now(), - "chatType" character varying(50) COLLATE pg_catalog."default" NOT NULL, - "chatName" character varying(255) COLLATE pg_catalog."default", - "gameId" uuid, - participants uuid[] NOT NULL, - CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Chats" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - type character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'direct'::character varying, - name character varying(255) COLLATE pg_catalog."default", - "gameId" uuid, - "createdBy" uuid, - users uuid[] NOT NULL, - messages json NOT NULL DEFAULT '[]'::json, - "lastActivity" timestamp without time zone, - "createDate" timestamp without time zone NOT NULL DEFAULT now(), - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - state integer NOT NULL DEFAULT 0, - "archiveDate" timestamp without time zone, - CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Contacts" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - name character varying(255) COLLATE pg_catalog."default" NOT NULL, - email character varying(255) COLLATE pg_catalog."default" NOT NULL, - userid uuid, - type integer NOT NULL, - txt text COLLATE pg_catalog."default" NOT NULL, - state integer NOT NULL DEFAULT 0, - "createDate" timestamp without time zone NOT NULL DEFAULT now(), - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - "adminResponse" text COLLATE pg_catalog."default", - "responseDate" timestamp without time zone, - "respondedBy" uuid, - CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Decks" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - name character varying(255) COLLATE pg_catalog."default" NOT NULL, - type integer NOT NULL, - user_id uuid NOT NULL, - creation_date timestamp without time zone NOT NULL DEFAULT now(), - cards json NOT NULL, - played_number integer NOT NULL DEFAULT 0, - ctype integer NOT NULL DEFAULT 0, - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - state integer NOT NULL DEFAULT 0, - organization_id uuid, - CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Games" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - gamecode character varying(10) COLLATE pg_catalog."default" NOT NULL, - maxplayers integer NOT NULL, - logintype integer NOT NULL DEFAULT 0, - boardsize integer NOT NULL DEFAULT 50, - "createdBy" uuid NOT NULL, - organizationid uuid, - decks jsonb NOT NULL DEFAULT '[]'::jsonb, - playerids uuid[] NOT NULL DEFAULT '{}'::uuid[], - "winnerId" uuid, - state integer NOT NULL DEFAULT 0, - "createDate" timestamp without time zone NOT NULL DEFAULT now(), - start_date timestamp without time zone, - "finishDate" timestamp without time zone, - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - "organizationId" uuid, - CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY (id), - CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE (gamecode) -); - -CREATE TABLE IF NOT EXISTS public."Organizations" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - name character varying(255) COLLATE pg_catalog."default" NOT NULL, - contactfname character varying(100) COLLATE pg_catalog."default" NOT NULL, - contactlname character varying(100) COLLATE pg_catalog."default" NOT NULL, - contactphone character varying(20) COLLATE pg_catalog."default" NOT NULL, - contactemail character varying(255) COLLATE pg_catalog."default" NOT NULL, - state integer NOT NULL DEFAULT 0, - regdate timestamp without time zone NOT NULL DEFAULT now(), - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - url character varying(500) COLLATE pg_catalog."default", - userinorg integer NOT NULL DEFAULT 0, - "maxOrganizationalDecks" integer, - CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS public."Users" -( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - orgid uuid, - username character varying(100) COLLATE pg_catalog."default" NOT NULL, - password character varying(255) COLLATE pg_catalog."default" NOT NULL, - email character varying(255) COLLATE pg_catalog."default" NOT NULL, - fname character varying(100) COLLATE pg_catalog."default" NOT NULL, - lname character varying(100) COLLATE pg_catalog."default" NOT NULL, - token character varying(255) COLLATE pg_catalog."default", - "TokenExpires" timestamp without time zone, - phone character varying(20) COLLATE pg_catalog."default", - state integer NOT NULL DEFAULT 0, - regdate timestamp without time zone NOT NULL DEFAULT now(), - "updateDate" timestamp without time zone NOT NULL DEFAULT now(), - "Orglogindate" timestamp without time zone, - CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY (id), - CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE (email), - CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE (username) -); - -CREATE TABLE IF NOT EXISTS public.migrations -( - id serial NOT NULL, - "timestamp" bigint NOT NULL, - name character varying COLLATE pg_catalog."default" NOT NULL, - CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id) -); - -ALTER TABLE IF EXISTS public."Decks" - ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY (organization_id) - REFERENCES public."Organizations" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - - -ALTER TABLE IF EXISTS public."Decks" - ADD CONSTRAINT "FK_a39059433e29882e1309d3a5e70" FOREIGN KEY (user_id) - REFERENCES public."Users" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - - -ALTER TABLE IF EXISTS public."Games" - ADD CONSTRAINT "FK_330362bff8b25bb573f31fb4023" FOREIGN KEY ("winnerId") - REFERENCES public."Users" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - - -ALTER TABLE IF EXISTS public."Games" - ADD CONSTRAINT "FK_e3c4e8898fa026a5551aefc4f62" FOREIGN KEY ("organizationId") - REFERENCES public."Organizations" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - - -ALTER TABLE IF EXISTS public."Games" - ADD CONSTRAINT "FK_f32db60863a8a393b30aa222cd5" FOREIGN KEY ("createdBy") - REFERENCES public."Users" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - -END; \ No newline at end of file diff --git a/SerpentRace_Frontend/.dockerignore b/SerpentRace_Frontend/.dockerignore deleted file mode 100644 index 51fba3e0..00000000 --- a/SerpentRace_Frontend/.dockerignore +++ /dev/null @@ -1,27 +0,0 @@ -node_modules -npm-debug.log -.git -.gitignore -README.md -.env -.env.local -.env.development.local -.env.test.local -.env.production.local -.cache -logs -*.log -.DS_Store -.vscode -.idea -*.swp -*.swo -dist -build -.next -.nuxt -.vuepress/dist -.serverless -.fusebox/ -.dynamodb/ -.tern-port diff --git a/SerpentRace_Frontend/.gitignore b/SerpentRace_Frontend/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/SerpentRace_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/SerpentRace_Frontend/CLEANUP_SUMMARY.md b/SerpentRace_Frontend/CLEANUP_SUMMARY.md deleted file mode 100644 index e6c9a58a..00000000 --- a/SerpentRace_Frontend/CLEANUP_SUMMARY.md +++ /dev/null @@ -1,96 +0,0 @@ -# ⚡ Gyors Összefoglaló - Felesleges Adatok Tisztítás - -## 🎯 Mi a probléma? - -A frontend **10 felesleges mezőt** küld a backendnek minden kártya mentésekor. - -## 📊 Számok - -- **Felesleges deck mezők:** 1 db (`description`) -- **Felesleges kártya mezők:** 9 db -- **Payload csökkenés:** ~32-60% -- **Implementációs idő:** ~3-4 óra - -## ✅ Használt mezők (BACKEND) - -```javascript -{ - name: "Pakli neve", - type: 2, // 0=LUCK, 1=JOKER, 2=QUESTION - ctype: 1, // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION - cards: [ - { - text: "Kérdés szövege", - type: 0, // CardType enum (0-4) - answer: "..." // TÍPUS-SPECIFIKUS formátum! - } - ] -} -``` - -## ❌ Felesleges mezők (TÖRLENDŐ) - -### Deck: -- `description` - nincs a backend sémában - -### Kártya: -- `id` (frontend generált) - backend UUID-t használ -- `question` - duplikáció (`text` használandó) -- `statement` - duplikáció (`text` használandó) -- `options` - `answer` array-ben kell lennie -- `correctAnswer` - `answer` array-ben kell lennie -- `leftItems`, `rightItems`, `correctPairs` - `answer` array-ben kell lennie -- `acceptedAnswers` - `answer` array-ként kell lennie -- `hint` - nincs implementálva - -## 🔄 Helyes answer formátumok - -| Típus | answer formátum | -|-------|----------------| -| QUIZ (0) | `[{answer: "A", text: "...", correct: true}, ...]` | -| PAIRING (1) | `[{left: "...", right: "..."}, ...]` | -| OWN_ANSWER (2) | `["answer1", "answer2", ...]` | -| TRUE_FALSE (3) | `true` vagy `false` | -| CLOSER (4) | `{correct: 123, percent: 10}` | - -## 🛠️ Következő lépések - -1. ✅ Olvasd el: `FRONTEND_TO_BACKEND_DATA_CLEANUP.md` -2. 🔧 Implementáld: `cardBackendConverter.js` utility -3. 🔄 Módosítsd: `DeckCreator.jsx` mentés logikát -4. ✅ Teszteld: minden kártyatípust - -## 📁 Kapcsolódó fájlok - -- **Részletes dokumentáció:** `FRONTEND_TO_BACKEND_DATA_CLEANUP.md` -- **Módosítandó frontend:** `src/pages/DeckCreator/DeckCreator.jsx` -- **Backend referencia:** `SerpentRace_Backend/src/Application/Services/CardProcessingService.ts` - ---- - -**Gyors példa:** - -```javascript -// ❌ ROSSZ (jelenleg) -{ - text: "Kérdés", - question: "Kérdés", // Duplikáció - options: ["A", "B", "C"], // Felesleges - correctAnswer: 0 // Felesleges -} - -// ✅ JÓ (célállapot) -{ - text: "Kérdés", - type: 0, - answer: [ - {answer: "A", text: "A", correct: true}, - {answer: "B", text: "B", correct: false}, - {answer: "C", text: "C", correct: false} - ] -} -``` - ---- - -📖 **Teljes dokumentáció:** Lásd `FRONTEND_TO_BACKEND_DATA_CLEANUP.md` diff --git a/SerpentRace_Frontend/README.md b/SerpentRace_Frontend/README.md deleted file mode 100644 index 7059a962..00000000 --- a/SerpentRace_Frontend/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# React + 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 using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/SerpentRace_Frontend/eslint.config.js b/SerpentRace_Frontend/eslint.config.js deleted file mode 100644 index ec2b712d..00000000 --- a/SerpentRace_Frontend/eslint.config.js +++ /dev/null @@ -1,33 +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' - -export default [ - { ignores: ['dist'] }, - { - files: ['**/*.{js,jsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - ecmaVersion: 'latest', - ecmaFeatures: { jsx: true }, - sourceType: 'module', - }, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - ...js.configs.recommended.rules, - ...reactHooks.configs.recommended.rules, - 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, - }, -] diff --git a/SerpentRace_Frontend/index.html b/SerpentRace_Frontend/index.html deleted file mode 100644 index 8ce9494b..00000000 --- a/SerpentRace_Frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - SerpentRace - - -
- - - diff --git a/SerpentRace_Frontend/nginx.conf b/SerpentRace_Frontend/nginx.conf deleted file mode 100644 index fa4630a8..00000000 --- a/SerpentRace_Frontend/nginx.conf +++ /dev/null @@ -1,60 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html index.htm; - - # Enable gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; - - # Security headers - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - - # Handle client routing - location / { - try_files $uri $uri/ /index.html; - } - - # API proxy to backend - location /api/ { - proxy_pass http://backend:3000/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_cache_bypass $http_upgrade; - } - - # WebSocket support - location /socket.io/ { - proxy_pass http://backend:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # Static assets caching - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } - - # Health check endpoint - location /health { - access_log off; - return 200 "healthy\n"; - add_header Content-Type text/plain; - } -} diff --git a/SerpentRace_Frontend/package-lock.json b/SerpentRace_Frontend/package-lock.json deleted file mode 100644 index 4470baf7..00000000 --- a/SerpentRace_Frontend/package-lock.json +++ /dev/null @@ -1,4002 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.0", - "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@tailwindcss/vite": "^4.1.7", - "axios": "^1.12.2", - "framer-motion": "^12.19.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-icons": "^5.5.0", - "react-router-dom": "^7.6.0", - "react-toastify": "^11.0.5", - "socket.io-client": "^4.8.1", - "tailwindcss": "^4.1.7" - }, - "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.4.1", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "terser": "^5.36.0", - "vite": "^6.3.5" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "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, - "license": "MIT", - "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.27.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", - "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.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "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, - "license": "MIT", - "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-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, - "license": "MIT", - "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.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", - "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.3.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "Apache-2.0", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", - "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/@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, - "license": "Apache-2.0", - "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, - "license": "Apache-2.0", - "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, - "license": "Apache-2.0", - "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, - "license": "Apache-2.0", - "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, - "license": "Apache-2.0", - "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==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "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==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "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/@tailwindcss/node": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz", - "integrity": "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==", - "license": "MIT", - "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.7" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.7.tgz", - "integrity": "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-x64": "4.1.7", - "@tailwindcss/oxide-freebsd-x64": "4.1.7", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.7", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-x64-musl": "4.1.7", - "@tailwindcss/oxide-wasm32-wasi": "4.1.7", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.7", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.7" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.7.tgz", - "integrity": "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.7.tgz", - "integrity": "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.7.tgz", - "integrity": "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.7.tgz", - "integrity": "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.7.tgz", - "integrity": "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.7.tgz", - "integrity": "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.7.tgz", - "integrity": "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz", - "integrity": "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz", - "integrity": "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.7.tgz", - "integrity": "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "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.9", - "@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.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz", - "integrity": "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz", - "integrity": "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz", - "integrity": "sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.1.7", - "@tailwindcss/oxide": "4.1.7", - "tailwindcss": "4.1.7" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6" - } - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT" - }, - "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.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", - "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", - "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@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" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "devOptional": 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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "Python-2.0" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "license": "MIT", - "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, - "license": "MIT" - }, - "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/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", - "dev": true, - "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" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "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/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "devOptional": true, - "license": "MIT" - }, - "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==", - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", - "dev": true, - "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" - } - ], - "license": "CC-BY-4.0" - }, - "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, - "license": "MIT", - "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==", - "license": "BlueOak-1.0.0", - "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==", - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT" - }, - "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==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "devOptional": true, - "license": "MIT" - }, - "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, - "license": "MIT" - }, - "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, - "license": "MIT" - }, - "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==", - "dev": true, - "license": "MIT" - }, - "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, - "license": "MIT", - "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, - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "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==", - "license": "Apache-2.0", - "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==", - "license": "MIT", - "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.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", - "dev": true, - "license": "ISC" - }, - "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.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "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, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", - "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.31.0", - "@eslint/plugin-kit": "^0.3.1", - "@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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "BSD-3-Clause", - "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, - "license": "BSD-2-Clause", - "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, - "license": "BSD-2-Clause", - "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, - "license": "MIT" - }, - "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, - "license": "MIT" - }, - "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, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "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, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "ISC" - }, - "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" - } - ], - "license": "MIT", - "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==", - "license": "MIT", - "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/framer-motion": { - "version": "12.19.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.19.1.tgz", - "integrity": "sha512-nq9hwWAEKf4gzprbOZzKugLV5OVKF7zrNDY6UOVu+4D3ZgIkg8L9Jy6AMrpBM06fhbKJ6LEG6UY5+t7Eq6wNlg==", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.19.0", - "motion-utils": "^12.19.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "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==", - "license": "MIT", - "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, - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "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==", - "license": "ISC" - }, - "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, - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.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.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "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, - "license": "MIT" - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT" - }, - "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, - "license": "MIT" - }, - "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, - "license": "MIT" - }, - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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==", - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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" - ], - "license": "MPL-2.0", - "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, - "license": "MIT", - "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, - "license": "MIT" - }, - "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, - "license": "ISC", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "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, - "license": "ISC", - "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==", - "license": "ISC", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/motion-dom": { - "version": "12.19.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.19.0.tgz", - "integrity": "sha512-m96uqq8VbwxFLU0mtmlsIVe8NGGSdpBvBSHbnnOJQxniPaabvVdGgxSamhuDwBsRhwX7xPxdICgVJlOpzn/5bw==", - "license": "MIT", - "dependencies": { - "motion-utils": "^12.19.0" - } - }, - "node_modules/motion-utils": { - "version": "12.19.0", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz", - "integrity": "sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/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" - } - ], - "license": "MIT", - "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, - "license": "MIT" - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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, - "license": "MIT", - "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==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "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" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "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, - "license": "MIT", - "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==", - "license": "MIT" - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.0" - } - }, - "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", - "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", - "license": "MIT", - "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.6.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.0.tgz", - "integrity": "sha512-DYgm6RDEuKdopSyGOWZGtDfSm7Aofb8CCzgkliTjtu/eDuB0gcsv6qdFhhi8HdtmA+KHkt5MfZ5K2PdzjugYsA==", - "license": "MIT", - "dependencies": { - "react-router": "7.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/react-toastify": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", - "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1" - }, - "peerDependencies": { - "react": "^18 || ^19", - "react-dom": "^18 || ^19" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "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, - "license": "ISC", - "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==", - "license": "MIT" - }, - "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": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.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, - "license": "MIT", - "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, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz", - "integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "license": "MIT", - "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==", - "license": "ISC", - "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==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "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==", - "license": "0BSD" - }, - "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, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.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==", - "dev": true, - "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" - } - ], - "license": "MIT", - "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, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "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/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, - "license": "MIT", - "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, - "license": "ISC" - }, - "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, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/SerpentRace_Frontend/package.json b/SerpentRace_Frontend/package.json deleted file mode 100644 index 3a4763d8..00000000 --- a/SerpentRace_Frontend/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@tailwindcss/vite": "^4.1.7", - "axios": "^1.12.2", - "framer-motion": "^12.19.1", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-icons": "^5.5.0", - "react-router-dom": "^7.6.0", - "react-toastify": "^11.0.5", - "socket.io-client": "^4.8.1", - "tailwindcss": "^4.1.7" - }, - "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.4.1", - "eslint": "^9.25.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^16.0.0", - "terser": "^5.36.0", - "vite": "^6.3.5" - } -} diff --git a/SerpentRace_Frontend/src/App.jsx b/SerpentRace_Frontend/src/App.jsx deleted file mode 100644 index 9a0c72b9..00000000 --- a/SerpentRace_Frontend/src/App.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useState, useEffect } from "react" -import { BrowserRouter as Router, Route, Routes } from "react-router-dom" -import { ROUTES } from "./utils/routes" -import AuthRegister from "./pages/Auth/AuthRegister" -import AuthLogin from "./pages/Auth/AuthLogin" -import Test from "./pages/Testing/Test" -import ForgotPassword from "./pages/Auth/ForgotPassword" -import ResetPassword from "./pages/Auth/ResetPassword" -import Landingpage from "./pages/Landing/Landingpage" -import Home from "./pages/Landing/Home" -import DeckManagerPage from "./pages/Decks/DeckManagerPage" -import Card_display from "./pages/Decks/Card_display" -import DeckCreator from "./pages/DeckCreator/DeckCreator" -import CompanyHub from "./pages/Contacts/Contacts" -import About from "./pages/About/About" -import ScrollToTop from "./components/ScrollToTop" -import GameScreen from "./pages/Game/GameScreen" -import GameTest from "./pages/Game/GameTest" -import Reports from "./pages/Report/Reports" -import Lobby from "./pages/Game/Lobby" -import ProfileCard from "./components/Userdetails/Userdetails" -import { ToastConfig } from "./components/Toastify/toastifyServices" // ✅ fontos: named import, nem default! -import VerifyEmailPage from "./pages/Auth/VerifyEmailPage" -import ChooseDeck from "./pages/Game/ChooseDeck" -import PlayerSetup from "./pages/Game/PlayerSetup" -import GameModalsDemo from "./pages/Game/GameModalsDemo" -import { GameWebSocketProvider } from "./contexts/GameWebSocketContext" - -function App() { - const [isMobile, setIsMobile] = useState(false) - - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth <= 1280) - } - - handleResize() - window.addEventListener("resize", handleResize) - - return () => window.removeEventListener("resize", handleResize) - }, []) - - // if (isMobile) { - // return ( - // - // - // } /> - // } /> - // } /> - // - // - // ); - // } - - return ( - <> - - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* } /> */} - } /> - } /> - } /> - - - - - {/* ✅ Toastify Container */} - - - ) -} - -export default App diff --git a/SerpentRace_Frontend/src/api/deckApi.js b/SerpentRace_Frontend/src/api/deckApi.js deleted file mode 100644 index 1a9460a7..00000000 --- a/SerpentRace_Frontend/src/api/deckApi.js +++ /dev/null @@ -1,58 +0,0 @@ -import { apiClient } from './userApi' - -// Create a new deck in the backend -export const createDeck = async (deck) => { - try { - const response = await apiClient.post('/decks', deck) - return response.data - } catch (err) { - throw err - } -} - -// Get paginated decks (authenticated) -export const getDecksPage = async (from = 0, to = 49) => { - try { - const response = await apiClient.get(`/decks/page/${from}/${to}`) - return response.data - } catch (err) { - throw err - } -} - -// Get a specific deck by ID (authenticated) -export const getDeckById = async (deckId) => { - try { - const response = await apiClient.get(`/decks/${deckId}`) - return response.data - } catch (err) { - throw err - } -} - -// Update an existing deck (authenticated) -export const updateDeck = async (deckId, deck) => { - try { - const response = await apiClient.patch(`/decks/${deckId}`, deck) - return response.data - } catch (err) { - throw err - } -} - -// Delete a deck (soft delete) (authenticated) -export const deleteDeck = async (deckId) => { - try { - const response = await apiClient.delete(`/decks/${deckId}`) - return response.data - } catch (err) { - throw err - } -} - -export default { - createDeck, - getDeckById, - updateDeck, - deleteDeck -} diff --git a/SerpentRace_Frontend/src/api/gameApi.js b/SerpentRace_Frontend/src/api/gameApi.js deleted file mode 100644 index 7a512847..00000000 --- a/SerpentRace_Frontend/src/api/gameApi.js +++ /dev/null @@ -1,80 +0,0 @@ -import { apiClient } from './userApi'; - -/** - * Create a new game - * @param {Object} gameData - Game creation data - * @param {string[]} gameData.deckids - Array of deck UUIDs - * @param {number} gameData.maxplayers - Maximum players (2-8) - * @param {number} gameData.logintype - 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION - * @returns {Promise} Game data with gameCode - */ -export const createGame = async (gameData) => { - try { - const response = await apiClient.post('/games/start', gameData); - return response.data; - } catch (error) { - console.error('Error creating game:', error); - throw error; - } -}; - -/** - * Join an existing game - * @param {Object} joinData - Join game data - * @param {string} joinData.gameCode - 6-character game code - * @param {string} [joinData.playerName] - Player name (required for public games) - * @returns {Promise} Game data with gameToken - */ -export const joinGame = async (joinData) => { - try { - const response = await apiClient.post('/games/join', joinData); - return response.data; - } catch (error) { - console.error('Error joining game:', error); - console.error('Join game error response:', error.response?.data); - throw error; - } -}; - -/** - * Start the game (gamemaster only) - * @param {string} gameId - Game UUID - * @returns {Promise} Game data with board - */ -export const startGame = async (gameId) => { - try { - const response = await apiClient.post(`/games/${gameId}/start`); - return response.data; - } catch (error) { - console.error('Error starting game:', error); - throw error; - } -}; - -/** - * Get user's games - * @returns {Promise} Array of games - */ -export const getMyGames = async () => { - try { - const response = await apiClient.get('/games/my-games'); - return response.data; - } catch (error) { - console.error('Error fetching games:', error); - throw error; - } -}; - -/** - * Get active public games - * @returns {Promise} Array of active games - */ -export const getActiveGames = async () => { - try { - const response = await apiClient.get('/games/active'); - return response.data; - } catch (error) { - console.error('Error fetching active games:', error); - throw error; - } -}; diff --git a/SerpentRace_Frontend/src/api/userApi.js b/SerpentRace_Frontend/src/api/userApi.js deleted file mode 100644 index af231c48..00000000 --- a/SerpentRace_Frontend/src/api/userApi.js +++ /dev/null @@ -1,109 +0,0 @@ -import axios from "axios" - -export const API_CONFIG = { - baseURL: (import.meta.env.VITE_API_URL ? import.meta.env.VITE_API_URL : "") + "/api", - wsURL: "http://localhost:3000", - timeout: 10000, - retryAttempts: 3, -} - -export const apiClient = axios.create({ - baseURL: API_CONFIG.baseURL, - timeout: API_CONFIG.timeout, - withCredentials: true, // Important for cookie-based auth - headers: { - "Content-Type": "application/json", - }, -}) - -//login -export const login = async (username, password) => { - try { - const response = await apiClient.post("/users/login", { username, password }) - return response - } catch (error) { - throw error - } -} - -//register -export const register = async (username, email, password, fname, lname, phone) => { - try { - const response = await apiClient.post("/users/create", { username, email, password, fname, lname, phone }) - return response - } catch (error) { - throw error - } -} - -// Get current user's game statistics -export const getUserStats = async () => { - try { - const response = await apiClient.get("/users/me/stats") - return response.data - } catch (error) { - throw error - } -} - - -// Email verification - POST -export const verifyEmail = async (token) => { - try { - const response = await apiClient.post(`/users/verify-email/${token}`); - return response; - } catch (error) { - throw error; - } -}; - -// Get current user profile -export const getUserProfile = async () => { - try { - const response = await apiClient.get("/users/profile"); - return response.data; - } catch (error) { - throw error; - } -}; - -// Update current user profile -export const updateUserProfile = async (data) => { - try { - const response = await apiClient.patch("/users/profile", data); - return response.data; - } catch (error) { - throw error; - } -}; - -// Delete current user profile -export const deleteUserProfile = async () => { - try { - const response = await apiClient.delete("/users/profile"); - return response.data; - } catch (error) { - throw error; - } -}; - -// Request password reset -export const forgotPassword = async (email) => { - try { - const response = await apiClient.post("/users/forgot-password", { email }); - return response.data; - } catch (error) { - throw error; - } -}; - -// Reset password with token -export const resetPassword = async (token, newPassword) => { - try { - const response = await apiClient.post("/users/reset-password", { token, newPassword }); - return response.data; - } catch (error) { - throw error; - } -}; - diff --git a/SerpentRace_Frontend/src/assets/SerpentRace_Animation/Path.module.css b/SerpentRace_Frontend/src/assets/SerpentRace_Animation/Path.module.css deleted file mode 100644 index fb29c73e..00000000 --- a/SerpentRace_Frontend/src/assets/SerpentRace_Animation/Path.module.css +++ /dev/null @@ -1,96 +0,0 @@ -.animation { - animation: fill 0.5s ease forwards 2.9s; -} - -.path0 { - stroke-dasharray: 603.0596923828125; - stroke-dashoffset: 603.0596923828125; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.45s; -} - -.path1 { - stroke-dasharray: 503.0904846191406; - stroke-dashoffset: 503.0904846191406; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.5s; -} - -.path2 { - stroke-dasharray: 625.779541015625; - stroke-dashoffset: 625.779541015625; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.45s; -} - -.path3 { - stroke-dasharray: 714.129638671875; - stroke-dashoffset: 714.129638671875; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.4s; -} - -.path4 { - stroke-dasharray: 427.98114013671875; - stroke-dashoffset: 427.98114013671875; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.35s; -} - -.path5 { - stroke-dasharray: 593.7645263671875; - stroke-dashoffset: 593.7645263671875; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.3s; -} - -.path6 { - stroke-dasharray: 603.0399780273438; - stroke-dashoffset: 603.0399780273438; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.25s; -} - -.path7 { - stroke-dasharray: 731.757568359375; - stroke-dashoffset: 731.757568359375; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.2s; -} - -.path8 { - stroke-dasharray: 382.3065185546875; - stroke-dashoffset: 382.3065185546875; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.2s; -} - -.path9 { - stroke-dasharray: 603.0382690429688; - stroke-dashoffset: 603.0382690429688; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.15s; -} - -.path10 { - stroke-dasharray: 652.2447509765625; - stroke-dashoffset: 652.2447509765625; - animation: draw 3s ease-in-out forwards; - animation-delay: 0.1s; -} - -@keyframes draw { - to { - stroke-dashoffset: 0; - } -} - -@keyframes fill { - from { - fill: transparent; - } - to { - fill: #ffffff; - } -} - diff --git a/SerpentRace_Frontend/src/assets/SerpentRace_Animation/SerpentRace_Animation.jsx b/SerpentRace_Frontend/src/assets/SerpentRace_Animation/SerpentRace_Animation.jsx deleted file mode 100644 index 376466b0..00000000 --- a/SerpentRace_Frontend/src/assets/SerpentRace_Animation/SerpentRace_Animation.jsx +++ /dev/null @@ -1,34 +0,0 @@ -// src/assets/SerpentRace_Animation/SerpentRace_Animation.jsx -// Animációs kiírás: SerpentRace - -import styles from "./Path.module.css"; -import React, { useRef } from "react"; - -const Animation = ({ sizePercentage = 100 }) => { - const width = (1253 * sizePercentage) / 100; - const height = (136 * sizePercentage) / 100; - - // 11 path-hoz refs - const pathRefs = Array.from({ length: 11 }, () => useRef(null)); - - return ( -
- {/* prettier-ignore */} - - - - - - - - - - - - - -
- ); -}; - -export default Animation; diff --git a/SerpentRace_Frontend/src/assets/backgrounds/Background.jsx b/SerpentRace_Frontend/src/assets/backgrounds/Background.jsx deleted file mode 100644 index d198d8a2..00000000 --- a/SerpentRace_Frontend/src/assets/backgrounds/Background.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useEffect, useState } from "react" -import { motion } from "framer-motion" - -const Background = () => { - const [gridSize, setGridSize] = useState({ cols: 12, rows: 6 }) - const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) - const [path, setPath] = useState([]) - - useEffect(() => { - const updateGrid = () => { - const width = window.innerWidth - const height = window.innerHeight - const cols = Math.max(8, Math.floor(width / 100)) - const rows = Math.max(5, Math.floor(height / 100)) - setGridSize({ cols, rows }) - } - - const handleMouseMove = (e) => { - setMousePos({ x: e.clientX, y: e.clientY }) - } - - updateGrid() - window.addEventListener("resize", updateGrid) - window.addEventListener("mousemove", handleMouseMove) - - return () => { - window.removeEventListener("resize", updateGrid) - window.removeEventListener("mousemove", handleMouseMove) - } - }, []) - - useEffect(() => { - const interval = setInterval(() => { - const newCol = Math.floor(Math.random() * gridSize.cols) - const newRow = Math.floor(Math.random() * gridSize.rows) - setPath((prevPath) => { - const newPath = [...prevPath, { col: newCol, row: newRow, opacity: 1 }] - if (newPath.length > 10) newPath.shift() - return newPath - }) - }, 500) - - const fadeInterval = setInterval(() => { - setPath((prevPath) => - prevPath - .map((point) => ({ ...point, opacity: Math.max(0, point.opacity - 0.05) })) - .filter((point) => point.opacity > 0) - ) - }, 100) - - return () => { - clearInterval(interval) - clearInterval(fadeInterval) - } - }, [gridSize]) - - return ( -
-
- {[...Array(gridSize.cols * gridSize.rows)].map((_, i) => { - const col = i % gridSize.cols - const row = Math.floor(i / gridSize.cols) - const cellX = (col + 0.5) * (window.innerWidth / gridSize.cols) - const cellY = (row + 0.5) * (window.innerHeight / gridSize.rows) - - const dx = cellX - mousePos.x - const dy = cellY - mousePos.y - const distance = Math.sqrt(dx * dx + dy * dy) - const distanceFactor = Math.max(0, 1 - distance / 300) - - const pathPoint = path.find((p) => p.col === col && p.row === row) - const pathOpacity = pathPoint ? pathPoint.opacity : 0 - - return ( - - ) - })} -
-
- ) -} - -export default Background diff --git a/SerpentRace_Frontend/src/assets/pictures/Logo.jsx b/SerpentRace_Frontend/src/assets/pictures/Logo.jsx deleted file mode 100644 index 28376797..00000000 --- a/SerpentRace_Frontend/src/assets/pictures/Logo.jsx +++ /dev/null @@ -1,18 +0,0 @@ -// src/assets/pictures/Logo.png -// Logo kép importálása és paraméterezése - -import React from 'react'; -import logo from './Logo.png'; - -const Logo = ({ size = 100 }) => ( - Logo -); - -export default Logo; - diff --git a/SerpentRace_Frontend/src/assets/pictures/Logo.png b/SerpentRace_Frontend/src/assets/pictures/Logo.png deleted file mode 100644 index 480d8c5d..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/Logo.png and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/LogoCard.jsx b/SerpentRace_Frontend/src/assets/pictures/LogoCard.jsx deleted file mode 100644 index a356f976..00000000 --- a/SerpentRace_Frontend/src/assets/pictures/LogoCard.jsx +++ /dev/null @@ -1,134 +0,0 @@ -import { useRef, useState } from "react" -import { motion, useMotionValue, useSpring } from "framer-motion" - -const springValues = { - damping: 30, - stiffness: 100, - mass: 2, -} - -export default function LogoCard({ - imageSrc, - altText = "Tilted card image", - captionText = "", - containerHeight = "300px", - containerWidth = "100%", - imageHeight = "300px", - imageWidth = "300px", - scaleOnHover = 1.1, - rotateAmplitude = 14, - showMobileWarning = true, - showTooltip = true, - overlayContent = null, - displayOverlayContent = false, -}) { - const ref = useRef(null) - const x = useMotionValue(0) - const y = useMotionValue(0) - const rotateX = useSpring(useMotionValue(0), springValues) - const rotateY = useSpring(useMotionValue(0), springValues) - const scale = useSpring(1, springValues) - const opacity = useSpring(0) - const rotateFigcaption = useSpring(0, { - stiffness: 350, - damping: 30, - mass: 1, - }) - - const [lastY, setLastY] = useState(0) - - function handleMouse(e) { - if (!ref.current) return - - const rect = ref.current.getBoundingClientRect() - const offsetX = e.clientX - rect.left - rect.width / 2 - const offsetY = e.clientY - rect.top - rect.height / 2 - - const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude - const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude - - rotateX.set(rotationX) - rotateY.set(rotationY) - - x.set(e.clientX - rect.left) - y.set(e.clientY - rect.top) - - const velocityY = offsetY - lastY - rotateFigcaption.set(-velocityY * 0.6) - setLastY(offsetY) - } - - function handleMouseEnter() { - scale.set(scaleOnHover) - opacity.set(1) - } - - function handleMouseLeave() { - opacity.set(0) - scale.set(1) - rotateX.set(0) - rotateY.set(0) - rotateFigcaption.set(0) - } - - return ( -
- {showMobileWarning && ( -
- This effect is not optimized for mobile. Check on desktop. -
- )} - - - - - {displayOverlayContent && overlayContent && ( - - {overlayContent} - - )} - - - {showTooltip && ( - - {captionText} - - )} -
- ) -} diff --git a/SerpentRace_Frontend/src/assets/pictures/busi.JPG b/SerpentRace_Frontend/src/assets/pictures/busi.JPG deleted file mode 100644 index ba062184..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/busi.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/donat.JPG b/SerpentRace_Frontend/src/assets/pictures/donat.JPG deleted file mode 100644 index d1fb7651..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/donat.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/gege.JPG b/SerpentRace_Frontend/src/assets/pictures/gege.JPG deleted file mode 100644 index cbef7bca..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/gege.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/piskor.JPG b/SerpentRace_Frontend/src/assets/pictures/piskor.JPG deleted file mode 100644 index 0d88b774..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/piskor.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/turo.JPG b/SerpentRace_Frontend/src/assets/pictures/turo.JPG deleted file mode 100644 index 07bf7ba8..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/turo.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/walke.JPG b/SerpentRace_Frontend/src/assets/pictures/walke.JPG deleted file mode 100644 index f8701e85..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/walke.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/assets/pictures/zsola.JPG b/SerpentRace_Frontend/src/assets/pictures/zsola.JPG deleted file mode 100644 index d210a8c6..00000000 Binary files a/SerpentRace_Frontend/src/assets/pictures/zsola.JPG and /dev/null differ diff --git a/SerpentRace_Frontend/src/components/Buttons/Button.jsx b/SerpentRace_Frontend/src/components/Buttons/Button.jsx deleted file mode 100644 index a658e91a..00000000 --- a/SerpentRace_Frontend/src/components/Buttons/Button.jsx +++ /dev/null @@ -1,22 +0,0 @@ -// src/components/Inputs/InputBox.jsx -// Gomb komponens - -import { motion } from "framer-motion" - -export default function Button({ text, type, onClick, width, className }) { - const widthClass = width ? width : "w-full" - - return ( - - {text} - - ) -} diff --git a/SerpentRace_Frontend/src/components/Buttons/ButtonDark.jsx b/SerpentRace_Frontend/src/components/Buttons/ButtonDark.jsx deleted file mode 100644 index 896e75d5..00000000 --- a/SerpentRace_Frontend/src/components/Buttons/ButtonDark.jsx +++ /dev/null @@ -1,20 +0,0 @@ -// src/components/Inputs/InputBox.jsx -// Gomb komponens - -import { motion } from "framer-motion" - -export default function Button({ text, type, onClick, width }) { - const widthClass = width ? width : "w-full" - - return ( - - {text} - - ) -} diff --git a/SerpentRace_Frontend/src/components/Buttons/ButtonGreen.jsx b/SerpentRace_Frontend/src/components/Buttons/ButtonGreen.jsx deleted file mode 100644 index 459348c6..00000000 --- a/SerpentRace_Frontend/src/components/Buttons/ButtonGreen.jsx +++ /dev/null @@ -1,20 +0,0 @@ -// src/components/Buttons/ButtonGreen.jsx -// Zöld gomb komponens (ButtonGreen) - -import { motion } from "framer-motion" - -export default function ButtonGreen({ text, type, onClick, width }) { - const widthClass = width ? width : "w-full" - - return ( - - {text} - - ) -} diff --git a/SerpentRace_Frontend/src/components/Card/Card.jsx b/SerpentRace_Frontend/src/components/Card/Card.jsx deleted file mode 100644 index 675a6cb6..00000000 --- a/SerpentRace_Frontend/src/components/Card/Card.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -export default function Card({ title, children, onClose }) { - return ( -
- - {title &&

{title}

} -
{children}
-
- ); -} - - diff --git a/SerpentRace_Frontend/src/components/DeckCreator/CardEditor.jsx b/SerpentRace_Frontend/src/components/DeckCreator/CardEditor.jsx deleted file mode 100644 index cc94232d..00000000 --- a/SerpentRace_Frontend/src/components/DeckCreator/CardEditor.jsx +++ /dev/null @@ -1,319 +0,0 @@ -// src/components/DeckCreator/CardEditor.jsx -// Jobb oldali kártya szerkesztő - -import React, { useState, useEffect } from "react" -import { FaSave, FaTimes, FaEye } from "react-icons/fa" -import TaskCardEditor from "./TaskCardEditor.jsx" -import JokerCardEditor from "./JokerCardEditor.jsx" -import LuckCardEditor from "./LuckCardEditor.jsx" -import CardPreview from "./CardPreview.jsx" -import { notifySuccess, notifyError,notifyWarning } from "../../components/Toastify/toastifyServices" - - -export default function CardEditor({ card, isCreating, cardType, onSave, onCancel }) { - const [cardData, setCardData] = useState(null) - const [showPreview, setShowPreview] = useState(false) - - // Alapértelmezett kártya adatok - const getDefaultCardData = (type) => { - const baseData = { - id: null, - type: type, - points: 10, - timeLimit: 30, - consequence: { type: 0, value: 1 } - } - - switch (type) { - case 'QUESTION': - return { - ...baseData, - subType: 'quiz', - question: '', - options: ['', '', '', ''], - correctAnswer: 0, - explanation: '', - acceptedAnswers: [''], - wrongConsequence: { type: 1, value: 1 } - } - case 'PAIRING': - case 'MATCHING': - return { - ...baseData, - type: 'QUESTION', - subType: 'matching', - taskDescription: '', - leftItems: ['', ''], - rightItems: ['', ''], - correctPairs: { 0: 0, 1: 1 }, - wrongConsequence: { type: 1, value: 1 } - } - case 'JOKER': - return { - ...baseData, - title: '', - description: '', - effect: '', - actionType: 'skip', - usage: 'once', - wrongConsequence: { type: 1, value: 1 } - } - case 'LUCK': - return { - ...baseData, - event: '', - positiveEffect: '', - negativeEffect: '', - probability: 50, - risk: 'low' - } - default: - return baseData - } - } - - // Kártya adatok inicializálása - useEffect(() => { - try { - if (isCreating && cardType) { - const defaultData = getDefaultCardData(cardType) - setCardData(defaultData) - } else if (card) { - setCardData({ ...card }) - } else { - setCardData(null) - } - } catch (error) { - console.error('Kártya inicializálási hiba:', error) - setCardData(null) - } - }, [card, isCreating, cardType]) - - const validateCard = (data) => { - try { - if (!data || !data.type) { - notifyError("Érvénytelen kártya adatok!") - return false - } - - if (data.type === 'QUESTION') { - // Quiz típus validálás - if (data.subType === 'quiz') { - if (!data.text || !data.text.trim()) { - notifyError("Kérdés megadása kötelező!") - return false - } - if (data.options && data.options.some(opt => !opt.trim())) { - notifyError("Minden válaszlehetőséget ki kell tölteni!") - return false - } - } - // Igaz/Hamis típus validálás - else if (data.subType === 'truefalse') { - if (!data.text || !data.text.trim()) { - notifyError("Állítás megadása kötelező!") - return false - } - if (data.isTrue === undefined || data.isTrue === null) { - notifyError("Válaszd ki, hogy az állítás igaz vagy hamis!") - return false - } - } - // Párosítás típus validálás - else if (data.subType === 'matching') { - if (!data.text || !data.text.trim()) { - notifyError("Feladat leírása kötelező!") - return false - } - if (!data.leftItems || data.leftItems.length === 0) { - notifyError("Legalább egy párosítást meg kell adni!") - return false - } - if (data.leftItems.some(item => !item.trim()) || data.rightItems.some(item => !item.trim())) { - notifyError("Minden párosítási elemet ki kell tölteni!") - return false - } - } - // Szöveges válasz típus validálás - else if (data.subType === 'text') { - if (!data.text || !data.text.trim()) { - notifyError("Kérdés megadása kötelező!") - return false - } - if (!data.acceptedAnswers || data.acceptedAnswers.length === 0 || data.acceptedAnswers.every(ans => !ans.trim())) { - notifyError("Legalább egy elfogadott választ meg kell adni!") - return false - } - } - // Általános validálás (ha nincs subType megadva) - else { - if (!data.text || !data.text.trim()) { - notifyError("Kérdés vagy állítás megadása kötelező!") - return false - } - } - } else if (data.type === 'JOKER') { - if (!data.text || !data.text.trim()) { - notifyError("Joker kártya szövege nem lehet üres!") - return false - } - } else if (data.type === 'LUCK') { - if (!data.text || !data.text.trim()) { - notifyError("Szerencse kártya szövege nem lehet üres!") - return false - } - } - - return true - } catch (error) { - console.error('Validálási hiba:', error) - notifyError("Hiba történt a kártya ellenőrzése során") - return false - } - } - - const updateCardData = (updates) => { - setCardData(prev => prev ? { ...prev, ...updates } : null) - } - - const handleSave = () => { - if (!cardData) { - notifyError("Nincs mentendő kártya adat!") - return - } - - if (!validateCard(cardData)) return - - onSave(cardData) - } - - // Ha nincs kiválasztott kártya vagy új kártya létrehozás - if (!cardData) { - return null - } - - return ( -
- {/* Type Mismatch Warning */} - {cardData?.type && cardType && cardData.type !== cardType && !isCreating && ( -
-
-
⚠️
-
-
- Figyelmeztetés: Nem megfelelő kártya típus -
-
- {`Ez egy ${ - cardData.type === 'QUESTION' ? 'Feladat' : - cardData.type === 'JOKER' ? 'Joker' : 'Szerencse' - } kártya, de a pakli típusa ${ - cardType === 'QUESTION' ? 'Feladat' : - cardType === 'JOKER' ? 'Joker' : 'Szerencse' - }.`} -
-
-
-
- )} - - {/* Header */} -
-
-
-
- {cardData.type === 'QUESTION' && '📋'} - {cardData.type === 'JOKER' && '🃏'} - {cardData.type === 'LUCK' && '🎲'} -
-
-

- {isCreating ? 'Új' : 'Szerkesztés'} {' '} - {(isCreating ? cardType : cardData.type) === 'QUESTION' && 'Feladat kártya'} - {(isCreating ? cardType : cardData.type) === 'JOKER' && 'Joker kártya'} - {(isCreating ? cardType : cardData.type) === 'LUCK' && 'Szerencse kártya'} -

-
- {cardData.type === 'QUESTION' && cardData.subType && ( - <> - {cardData.subType === 'quiz' && 'Quiz (A/B/C/D)'} - {cardData.subType === 'truefalse' && 'Igaz/Hamis'} - {cardData.subType === 'matching' && 'Párosítás'} - {cardData.subType === 'text' && 'Szöveges válasz'} - - )} -
-
-
- -
- - - - - -
-
-
- - {/* Content */} -
- {showPreview ? ( -
- -
- ) : ( -
- {cardData.type === 'QUESTION' && ( - - )} - - {cardData.type === 'JOKER' && ( - - )} - - {cardData.type === 'LUCK' && ( - - )} -
- )} -
-
- ) -} diff --git a/SerpentRace_Frontend/src/components/DeckCreator/CardPreview.jsx b/SerpentRace_Frontend/src/components/DeckCreator/CardPreview.jsx deleted file mode 100644 index 5835bfe8..00000000 --- a/SerpentRace_Frontend/src/components/DeckCreator/CardPreview.jsx +++ /dev/null @@ -1,148 +0,0 @@ -// src/components/DeckCreator/CardPreview.jsx -// Kártya előnézet komponens - -import React from "react" -import { FaQuestionCircle, FaTheaterMasks, FaDice, FaClock, FaStar } from "react-icons/fa" - -export default function CardPreview({ card }) { - if (!card) { - return ( -
-
🃏
-
Nincs kiválasztott kártya az előnézethez
-
- ) - } - - // Kártya típus specifikus beállítások - const getCardConfig = (card) => { - switch (card.type) { - case 'task': - return { - bgColor: 'var(--color-question)', - icon: FaQuestionCircle, - title: 'FELADAT KÁRTYA', - emoji: '📋' - } - case 'joker': - return { - bgColor: 'var(--color-fun)', - icon: FaTheaterMasks, - title: 'JOKER KÁRTYA', - emoji: '🎭' - } - case 'luck': - return { - bgColor: 'var(--color-luck)', - icon: FaDice, - title: 'SZERENCSE KÁRTYA', - emoji: '🎲' - } - default: - return { - bgColor: 'var(--color-border)', - icon: FaQuestionCircle, - title: 'ISMERETLEN KÁRTYA', - emoji: '❓' - } - } - } - - const config = getCardConfig(card) - - // Kártya tartalom meghatározása - const getCardContent = (card) => { - if (card.type === 'task') { - return card.question || card.statement || 'Feladat leírása...' - } - if (card.type === 'joker' || card.type === 'luck') { - return card.text || 'Kártya szövege...' - } - return 'Kártya tartalma...' - } - - return ( -
- {/* Kártya container */} -
- {/* Kártya header */} -
- {/* Háttér pattern */} -
-
-
- -
- - - {config.title} - -
-
- - {/* Kártya body */} -
- {/* Főikon */} -
-
{config.emoji}
-
- - {/* Tartalom */} -
-
100 ? '14px' : '16px' - }} - > - {getCardContent(card)} -
-
- - {/* Alsó információk */} -
-
- {/* Idő */} - {card.timeLimit && ( -
- - {card.timeLimit}s -
- )} - - {/* Pontok */} - {card.points && ( -
- - {card.points} pont -
- )} - - {/* Ha nincs idő/pont info */} - {!card.timeLimit && !card.points && ( -
- SerpentRace Deck -
- )} -
-
-
- - {/* Kártya corner dekoráció */} -
-
-
-
- ) -} \ No newline at end of file diff --git a/SerpentRace_Frontend/src/components/DeckCreator/CardsList.jsx b/SerpentRace_Frontend/src/components/DeckCreator/CardsList.jsx deleted file mode 100644 index 30ea9d16..00000000 --- a/SerpentRace_Frontend/src/components/DeckCreator/CardsList.jsx +++ /dev/null @@ -1,270 +0,0 @@ -// src/components/DeckCreator/CardsList.jsx -// Bal oldali kártyák listája és új kártya létrehozás - -import React, { useState } from "react" -import { - FaPlus, - FaEdit, - FaTrash, - FaQuestionCircle, - FaCheck, - FaTimes, - FaDice, - FaTheaterMasks -} from "react-icons/fa" -import { notifySuccess, notifyError } from "../../components/Toastify/toastifyServices" - -const cardTypeIcons = { - QUESTION: { icon: FaQuestionCircle, color: "var(--color-question)" }, - JOKER: { icon: FaTheaterMasks, color: "var(--color-fun)" }, - LUCK: { icon: FaDice, color: "var(--color-luck)" } -} - -const cardSubTypeLabels = { - quiz: "Quiz", - truefalse: "Igaz/Hamis", - matching: "Párosítás", - text: "Szöveges válasz" -} - -export default function CardsList({ - cards, - selectedCard, - deckType, - onSelectCard, - onCreateCard, - onDeleteCard, - isCreatingCard, - newCardType -}) { - const [confirmingDelete, setConfirmingDelete] = useState(null) - - const getCardPreview = (card) => { - if (card.type === 'QUESTION') { - return card.question || card.statement || 'Új feladat kártya' - } - if (card.type === 'JOKER') { - return card.text || 'Új joker kártya' - } - if (card.type === 'LUCK') { - return card.text || 'Új szerencse kártya' - } - return "Ismeretlen kártya" - } - - const getCardTypeLabel = (card) => { - if (card.type === 'QUESTION') { - if (card.subType) { - return cardSubTypeLabels[card.subType] || "Feladat" - } - return "Feladat" - } - if (card.type === 'JOKER') { - return 'Joker' - } - if (card.type === 'LUCK') { - return 'Szerencse' - } - return "Ismeretlen" - } - - const handleConfirmDelete = () => { - if (confirmingDelete) { - onDeleteCard(confirmingDelete) - notifySuccess("Kártya sikeresen törölve a pakliból!") - setConfirmingDelete(null) - } - } - - const handleCancelDelete = () => { - setConfirmingDelete(null) - } - - return ( -
- {/* Header */} -
-

- 🃏 Kártyák -

- - {/* New Card Button */} - -
- - {/* Cards List */} -
- {/* Creating Card Indicator */} - {isCreatingCard && ( -
-
- {newCardType && ( -
- {React.createElement(cardTypeIcons[newCardType]?.icon || FaQuestionCircle, { - className: "text-[color:var(--color-success)] text-sm" - })} -
- )} -
-
- Új {newCardType === "QUESTION" ? "feladat" : newCardType === "JOKER" ? "joker" : "szerencse"} kártya -
-
- Szerkesztés folyamatban... -
-
-
-
- )} - - {/* Existing Cards */} - {cards.map((card, index) => { - const cardIcon = cardTypeIcons[card.type] || cardTypeIcons.task - const isSelected = selectedCard?.id === card.id - - return ( -
onSelectCard(card)} - className={` - p-4 rounded-xl border cursor-pointer transition-all duration-200 hover:scale-105 group relative - ${ - isSelected - ? "bg-[color:var(--color-success)]/10 border-[color:var(--color-success)] shadow-lg" - : "bg-[color:var(--color-background)]/50 border-[color:var(--color-surface-selected)] hover:bg-[color:var(--color-background)]/80" - } - ${card.type !== deckType ? "opacity-70" : ""} - `} - > - {card.type !== deckType && ( -
-
-
- - ⚠️ Nem megfelelő típus - -
- )} - {/* Card Header */} -
-
-
- {React.createElement(cardIcon.icon, { - style: { color: cardIcon.color }, - className: "text-lg" - })} -
- -
-
- #{index + 1} - {getCardTypeLabel(card)} -
- {card.timeLimit && ( -
- ⏱️ {card.timeLimit} másodperc -
- )} -
-
- - {/* Action Buttons */} -
- -
-
- - {/* Card Content Preview */} -
-
- {getCardPreview(card)} -
-
-
- ) - })} - - {/* Empty State */} - {cards.length === 0 && !isCreatingCard && ( -
-
🃏
-
- Még nincsenek kártyák. -
- Hozz létre az első kártyát! -
-
- )} -
- - {/* Confirm Delete Popup */} - {confirmingDelete && ( -
-
-

- Biztosan törölni szeretnéd? -

-

- Ez a művelet nem visszavonható. -

-
- - -
-
-
- )} - - {/* Footer Stats */} -
-
-
- 📊 Összesen: {cards.length} kártya -
-
-
-
- ) -} diff --git a/SerpentRace_Frontend/src/components/DeckCreator/DeckHeader.jsx b/SerpentRace_Frontend/src/components/DeckCreator/DeckHeader.jsx deleted file mode 100644 index ad1f32ff..00000000 --- a/SerpentRace_Frontend/src/components/DeckCreator/DeckHeader.jsx +++ /dev/null @@ -1,246 +0,0 @@ -// src/components/DeckCreator/DeckHeader.jsx -// Deck alapadatok szerkesztése és mentés - -import React, { useState, useRef, useEffect } from "react" -import { FaSave, FaArrowLeft, FaGlobe, FaLock, FaQuestionCircle, FaDice, FaLaughBeam, FaTrash, FaChevronDown, FaChevronUp } from "react-icons/fa" - -const deckTypes = [ - { value: "QUESTION", label: "Kérdés", icon: FaQuestionCircle, color: "var(--color-question)" }, - { value: "LUCK", label: "Szerencse", icon: FaDice, color: "var(--color-luck)" }, - { value: "JOKER", label: "Joker", icon: FaLaughBeam, color: "var(--color-fun)" } -] - -const privacyOptions = [ - { value: "private", label: "Privát", icon: FaLock }, - { value: "public", label: "Publikus", icon: FaGlobe } -] - -export default function DeckHeader({ deck, onUpdate, onSave, onBack, onDelete }) { - const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false); - const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false); - const [isDetailsExpanded, setIsDetailsExpanded] = useState(false); - const typeDropdownRef = useRef(null); - const privacyDropdownRef = useRef(null); - - const currentDeckType = deckTypes.find(type => type.value === deck.type) || deckTypes[0] - const currentPrivacy = privacyOptions.find(option => option.value === deck.privacy) || privacyOptions[0] - - useEffect(() => { - function handleClickOutside(event) { - if (typeDropdownRef.current && !typeDropdownRef.current.contains(event.target)) { - setIsTypeDropdownOpen(false); - } - if (privacyDropdownRef.current && !privacyDropdownRef.current.contains(event.target)) { - setIsPrivacyDropdownOpen(false); - } - } - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - const handleInputChange = (field, value) => { - onUpdate({ [field]: value }) - } - - // Remove unused card count variables - - return ( -
- {/* Top Row - Title and Actions */} -
-
- - -

- 📝 Pakli Szerkesztés -

-
- -
- {deck.id && ( - - )} - - -
-
- - {/* Collapsible Details Section */} -
- - - {isDetailsExpanded && ( -
- {/* Two Column Layout */} -
- {/* Deck Name - Takes up 2 columns */} -
- - handleInputChange('name', e.target.value)} - className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200" - placeholder="Add meg a pakli nevét..." - /> -
- - {/* Empty space for visual balance */} -
-
- - {/* Type, Privacy and Description Row */} -
- {/* Deck Type */} -
- -
- - - {isTypeDropdownOpen && ( -
- {deckTypes.map(type => ( - - ))} -
- )} -
-
- - {/* Privacy */} -
- -
- - - {isPrivacyDropdownOpen && ( -
- {privacyOptions.map(option => ( - - ))} -
- )} -
-
- - {/* Description */} -
- - handleInputChange('description', e.target.value)} - className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200" - placeholder="Rövid leírás..." - /> -
-
-
- )} -
-
- ) -} \ No newline at end of file diff --git a/SerpentRace_Frontend/src/components/DeckCreator/DeckManager.jsx b/SerpentRace_Frontend/src/components/DeckCreator/DeckManager.jsx deleted file mode 100644 index 4f1efb16..00000000 --- a/SerpentRace_Frontend/src/components/DeckCreator/DeckManager.jsx +++ /dev/null @@ -1,445 +0,0 @@ -import React, { useState, useEffect } from "react" -import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate" -import { - FaPlus, - FaFilter, - FaCalendarAlt, - FaArrowUp, - FaArrowDown, - FaSortAlphaDown, - FaSortAlphaUp, - FaQuestionCircle, - FaChevronLeft, - FaChevronRight, -} from "react-icons/fa" -import SearchBox from "../Search/SearchBox" -import PopUp from "../PopUp/PopUp" -import DeckInfoPopUp from "../PopUp/DeckInfoPopUp" - -const deckTypes = [ - { label: "Luck", color: "var(--color-luck)" }, - { label: "Question", color: "var(--color-question)" }, - { label: "Joker", color: "var(--color-fun)" }, -] - -// initial state will be fetched from backend - -const origins = ["Mind", "Vállalati", "Saját"] - -const sortOptions = [ - { - value: "date-asc", - label: ( - <> - - - - ), - }, - { - value: "date-desc", - label: ( - <> - - - - ), - }, - { - value: "abc-asc", - label: ( - <> - - - ), - }, - { - value: "abc-desc", - label: ( - <> - - - ), - }, -] - -const DeckManager = () => { - const { goDeckCreator } = HandleNavigate() - - const [selectedType, setSelectedType] = useState("All") - const [selectedOrigin, setSelectedOrigin] = useState("Mind") - const [sortBy, setSortBy] = useState("date-desc") - const [search, setSearch] = useState("") - const [showSortHelp, setShowSortHelp] = useState(false) - const [selectedDeck, setSelectedDeck] = useState(null) - const [allDecks, setAllDecks] = useState([]) // Összes pakli - const [loading, setLoading] = useState(false) - const [itemsPerPage, setItemsPerPage] = useState(20) - const [currentPage, setCurrentPage] = useState(1) - - // Load all decks once - useEffect(() => { - let mounted = true - const load = async () => { - setLoading(true) - try { - // Load all decks (0-99 is the max limit = 100 decks) - const result = await import('../../api/deckApi').then(m => m.getDecksPage(0, 99)) - if (!mounted) return - - console.log('Loaded decks:', result) // Debug - - // Map backend deck shape to UI shape - const mapped = (result.decks || []).map(d => ({ - id: d.id, - name: d.name, - type: d.type === 2 ? 'Question' : d.type === 1 ? 'Joker' : 'Luck', - created: d.creationdate ? new Date(d.creationdate).toLocaleDateString() : '', - origin: d.ctype === 2 ? 'Vállalati' : d.ctype === 0 ? 'Mind' : 'Saját', - raw: d - })) - - console.log('Mapped decks:', mapped) // Debug - setAllDecks(mapped) - } catch (err) { - console.error('Failed to load decks', err) - } finally { - setLoading(false) - } - } - load() - return () => { mounted = false } - }, []) - - // Filter logic - let filteredDecks = allDecks.filter((deck) => { - const typeMatch = selectedType === "All" || deck.type === selectedType - const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin - const searchMatch = !search || deck.name.toLowerCase().includes(search.toLowerCase()) - return typeMatch && originMatch && searchMatch - }) - - // Sort logic - filteredDecks = [...filteredDecks].sort((a, b) => { - if (sortBy === "date-asc") { - return a.created.localeCompare(b.created) - } else if (sortBy === "date-desc") { - return b.created.localeCompare(a.created) - } else if (sortBy === "abc-asc") { - return a.name.localeCompare(b.name) - } else if (sortBy === "abc-desc") { - return b.name.localeCompare(a.name) - } - return 0 - }) - - // Pagination logic - frontend only - const totalDecks = filteredDecks.length - const totalPages = Math.ceil(totalDecks / itemsPerPage) - const startIndex = (currentPage - 1) * itemsPerPage - const endIndex = startIndex + itemsPerPage - const paginatedDecks = filteredDecks.slice(startIndex, endIndex) - - // Reset to page 1 when filters or items per page change - useEffect(() => { - setCurrentPage(1) - }, [selectedType, selectedOrigin, search, sortBy, itemsPerPage]) - - return ( -
-
- {/* Filters */} -
-
- setSearch(e.target.value)} - width={240} - placeholder="Keresés..." - className="mr-4" - /> - - Típus: - - {deckTypes.map((type) => ( - - ))} - Eredet: - - - Rendezés: - - - - {showSortHelp && ( - setShowSortHelp(false)}> -

Rendezési lehetőségek magyarázata

-
    -
  • - 📅↑ – Dátum szerint növekvő sorrendben (legrégebbi - elöl) -
  • -
  • - 📅↓ – Dátum szerint csökkenő sorrendben (legújabb elöl) -
  • -
  • - A→Z – Név szerint növekvő sorrendben (A-tól Z-ig) -
  • -
  • - Z→A – Név szerint csökkenő sorrendben (Z-től A-ig) -
  • -
- -
- )} -
-
- - {/* Items per page selector and pagination info */} -
-
- - Elemek oldalanként: - - -
- -
- {totalDecks > 0 ? ( - <> - {startIndex + 1}-{Math.min(endIndex, totalDecks)} / {totalDecks} pakli - - ) : ( - <>0 pakli - )} -
-
- - {/* Decks Grid */} -
- {/* Create New Deck (Mockup) */} -
goDeckCreator()} - className="flex flex-col items-center justify-center h-40 sm:h-48 bg-[color:var(--color-card)] border-2 border-dashed border-[color:var(--color-success)] rounded-xl sm:rounded-2xl cursor-pointer hover:bg-[color:var(--color-success)]/20 transition-all duration-200 shadow-lg" - > - - Új pakli létrehozása -
- {/* Existing Decks (from backend) */} - {loading && ( -
Betöltés...
- )} - {!loading && filteredDecks.length === 0 && ( -
Nincsenek mentett paklik.
- )} - {!loading && paginatedDecks.map((deck) => { - const deckType = deckTypes.find((t) => t.label === deck.type) - const borderColor = deckType ? deckType.color : "var(--color-success)" - return ( -
setSelectedDeck(deck)} - > -
- - {deck.type === "Luck" - ? "Szerencse" - : deck.type === "Question" - ? "Kérdés" - : deck.type === "Fun" - ? "Joker" - : deck.type} - -

- {deck.name} -

-
-
- Létrehozva: {deck.created} -
-
- ) - })} -
- - {/* Pagination Controls */} - {totalPages > 1 && ( -
- - -
- {[...Array(totalPages)].map((_, index) => { - const pageNum = index + 1 - // Show first page, last page, current page and neighbors - if ( - pageNum === 1 || - pageNum === totalPages || - (pageNum >= currentPage - 1 && pageNum <= currentPage + 1) - ) { - return ( - - ) - } else if ( - pageNum === currentPage - 2 || - pageNum === currentPage + 2 - ) { - return ( - - ... - - ) - } - return null - })} -
- - -
- )} -
- - {/* Deck Info Popup */} - {selectedDeck && setSelectedDeck(null)} />} -
- ) -} - -export default DeckManager diff --git a/SerpentRace_Frontend/src/components/DeckCreator/JokerCardEditor.jsx b/SerpentRace_Frontend/src/components/DeckCreator/JokerCardEditor.jsx deleted file mode 100644 index 7f87e728..00000000 --- a/SerpentRace_Frontend/src/components/DeckCreator/JokerCardEditor.jsx +++ /dev/null @@ -1,149 +0,0 @@ -// src/components/DeckCreator/JokerCardEditor.jsx -// Joker kártya szerkesztő - -import React, { useState, useEffect } from 'react' -import { FaTheaterMasks, FaInfoCircle, FaUsers } from 'react-icons/fa' - -export default function JokerCardEditor({ card, onChange }) { - const [cardData, setCardData] = useState({ - type: 'JOKER', - text: '' - }) - - useEffect(() => { - if (card) { - setCardData({ - type: 'JOKER', - text: card.text || '' - }) - } - }, [card]) - - const handleTextChange = (e) => { - const newCardData = { - ...cardData, - text: e.target.value - } - setCardData(newCardData) - - if (onChange) { - onChange(newCardData) - } - } - - // Példa joker kártyák - const exampleCards = [ - "Felelsz vagy mersz? (Az előző játékos kérdez)", - "Csinálj 20 felülést!", - "Mesélj el egy vicces történetet az életedből!", - "Utánozd a kedvenc állatodat 30 másodpercig!", - "Énekelj el egy dalt amit mindenki ismer!", - "Mondj el 5 dolgot amiért hálás vagy!", - "Táncolj 1 percig zene nélkül!" - ] - - const insertExample = (example) => { - setCardData(prev => ({ - ...prev, - text: example - })) - - if (onChange) { - onChange({ - ...cardData, - text: example - }) - } - } - - return ( -
- {/* Info box */} -
-
-
- -
-

- - Joker kártya működése: -

-

- A joker kártyák interaktív feladatokat tartalmaznak, melyek megtörik a jeget a játékosok között. - Ezek lehetnek fizikai feladatok, kérdések, vagy szórakoztató kihívások. -

-

- Cél: Szórakozás és játékosok közötti kapcsolat erősítése -

-
-
-
-
- - {/* Kártya szövege */} -
-

- - Kártya szövege -

- -
- -