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/SerpentRace_Backend/node_modules/jest/bin/jest.js b/SerpentRace_Backend/node_modules/jest/bin/jest.js old mode 100644 new mode 100755 diff --git a/SerpentRace_Backend/src/Api/index.ts b/SerpentRace_Backend/src/Api/index.ts deleted file mode 100644 index ff8cfde8..00000000 --- a/SerpentRace_Backend/src/Api/index.ts +++ /dev/null @@ -1,285 +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; -let server: any; // Declare server variable - -// Initialize database connection and start server -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); - }); - - // Start server with WebSocket support AFTER database is ready - 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` - } - }); - }); - }) - .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); - }); - -// 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/Application/Services/LoggingService.ts b/SerpentRace_Backend/src/Application/Services/LoggingService.ts deleted file mode 100644 index fcafc7ce..00000000 --- a/SerpentRace_Backend/src/Application/Services/LoggingService.ts +++ /dev/null @@ -1,422 +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().catch(error => { - console.warn('MinIO bucket initialization failed:', error.message); - }); - } 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().catch(error => { - console.warn('MinIO bucket initialization failed:', error.message); - }); - } 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.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/WebSocketService.ts b/SerpentRace_Backend/src/Application/Services/WebSocketService.ts deleted file mode 100644 index 0ef9036e..00000000 --- a/SerpentRace_Backend/src/Application/Services/WebSocketService.ts +++ /dev/null @@ -1,1410 +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 (handle async without await in constructor) - this.initializeRedis().catch(error => { - logError('Redis initialization failed in WebSocketService constructor', error as Error); - }); - - 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/tsconfig.json b/SerpentRace_Backend/tsconfig.json deleted file mode 100644 index e3af7671..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": [], /* 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/deployment/.env.server b/SerpentRace_Docker/deployment/.env.server deleted file mode 100644 index 60be1d1e..00000000 --- a/SerpentRace_Docker/deployment/.env.server +++ /dev/null @@ -1,71 +0,0 @@ -# SerpentRace Production Server Environment Variables -# IMPORTANT: Change all placeholder values before deployment! - -# Production settings -NODE_ENV=production - -# Database Configuration -DB_HOST=postgres -DB_PORT=5432 -DB_NAME=serpentrace -DB_USERNAME=postgres -# CHANGE THIS: Use a strong password -POSTGRES_PASSWORD=CHANGE_THIS_STRONG_DATABASE_PASSWORD_123! - -# Redis Configuration -REDIS_URL=redis://redis:6379 -REDIS_HOST=redis -REDIS_PORT=6379 -# CHANGE THIS: Set a Redis password for security -REDIS_PASSWORD=CHANGE_THIS_REDIS_PASSWORD_123! - -# JWT Configuration -# CHANGE THIS: Use a strong secret key (minimum 32 characters) -JWT_SECRET=CHANGE_THIS_JWT_SECRET_KEY_MINIMUM_32_CHARACTERS_FOR_PRODUCTION_SECURITY -JWT_EXPIRY=86400 -JWT_EXPIRATION=24h -JWT_REFRESH_EXPIRATION=7d - -# Email Configuration (SMTP) -# CHANGE THESE: Configure your email provider -EMAIL_HOST=smtp.yourmailprovider.com -EMAIL_PORT=587 -EMAIL_SECURE=false -EMAIL_USER=your_email@yourdomain.com -EMAIL_PASS=your_email_password -EMAIL_FROM="SerpentRace " - -# MinIO Object Storage -MINIO_ENDPOINT=minio -MINIO_PORT=9000 -MINIO_USE_SSL=false -# CHANGE THESE: Use strong credentials -MINIO_ACCESS_KEY=serpentrace_admin -MINIO_SECRET_KEY=CHANGE_THIS_MINIO_SECRET_KEY_123! -MINIO_BUCKET_NAME=serpentrace-logs - -# Application Settings -APP_BASE_URL=http://your-domain.com -PORT=3000 - -# Chat System Limits -CHAT_INACTIVITY_TIMEOUT_MINUTES=30 -CHAT_MAX_MESSAGES_PER_USER=100 -CHAT_MESSAGE_CLEANUP_WEEKS=4 - -# Logging -MAX_LOGS_PER_FILE=10000 - -# SSL/TLS Configuration (if using HTTPS) -# Uncomment and configure if you have SSL certificates -# SSL_CERT_PATH=/path/to/certificate.crt -# SSL_KEY_PATH=/path/to/private.key -# SSL_CA_PATH=/path/to/ca-bundle.crt - -# Security Headers (already configured in nginx) -# These are handled by the nginx configuration - -# Backup Configuration (optional) -# BACKUP_ENABLED=true -# BACKUP_SCHEDULE=0 2 * * * -# BACKUP_RETENTION_DAYS=30 \ No newline at end of file diff --git a/SerpentRace_Docker/deployment/README.md b/SerpentRace_Docker/deployment/README.md deleted file mode 100644 index 97a0a4bc..00000000 --- a/SerpentRace_Docker/deployment/README.md +++ /dev/null @@ -1,203 +0,0 @@ -# SerpentRace Production Deployment Guide - -## Overview -This package contains everything needed to deploy SerpentRace in a production environment using pre-built Docker images. - -## Package Contents -- `serpentRaceDocker.tar` - All Docker images packed for deployment -- `docker-compose.deploy.yml` - Production Docker Compose configuration -- `.env.server` - Environment variables template for production -- `load-images.bat` - Automated deployment script for Windows servers -- `README.md` - This deployment guide - -## System Requirements -- Windows Server 2016+ or Windows 10/11 -- Docker Desktop or Docker Engine -- Docker Compose -- Minimum 4GB RAM, 20GB free disk space -- Network ports: 80, 443, 3000, 5432, 6379, 9000, 9001 - -## Pre-Deployment Configuration - -### 1. Environment Variables -Edit `.env.server` and update the following **REQUIRED** settings: - -```bash -# Database - Use a strong password -POSTGRES_PASSWORD=your_strong_database_password - -# JWT Security - Use a random 32+ character string -JWT_SECRET=your_super_secret_jwt_key_32_chars_minimum - -# Redis Security -REDIS_PASSWORD=your_redis_password - -# MinIO Storage -MINIO_ACCESS_KEY=your_minio_admin_user -MINIO_SECRET_KEY=your_minio_secret_key - -# Email Configuration (for notifications) -EMAIL_HOST=smtp.yourmailprovider.com -EMAIL_USER=your_email@yourdomain.com -EMAIL_PASS=your_email_password -EMAIL_FROM="SerpentRace " - -# Application URL -APP_BASE_URL=http://your-domain.com -``` - -### 2. Security Checklist -- [ ] Changed all default passwords -- [ ] Generated strong JWT secret (32+ characters) -- [ ] Configured email settings -- [ ] Updated domain name in APP_BASE_URL -- [ ] Configured firewall rules -- [ ] Planned SSL certificate setup - -## Deployment Steps - -### Automatic Deployment (Recommended) -1. Extract all files to your server directory -2. Edit `.env.server` with your configuration -3. Run `load-images.bat` -4. Follow the prompts - -### Manual Deployment -1. Load Docker images: - ```cmd - docker load -i serpentRaceDocker.tar - ``` - -2. Start services: - ```cmd - docker-compose -f docker-compose.deploy.yml --env-file .env.server up -d - ``` - -## Post-Deployment - -### Verify Services -Check that all services are running: -```cmd -docker-compose -f docker-compose.deploy.yml ps -``` - -### Access Points -- **Frontend Application**: http://localhost (or your domain) -- **Backend API**: http://localhost:3000 -- **MinIO Console**: http://localhost:9001 -- **Database**: localhost:5432 (internal access only) - -### Initial Setup -1. Access the frontend and verify it loads -2. Test user registration and login -3. Check backend API health: http://localhost:3000/health -4. Access MinIO console to verify storage - -### Security Hardening - -#### Firewall Configuration -Open only necessary ports: -- Port 80 (HTTP) - Public -- Port 443 (HTTPS) - Public (when SSL configured) -- Ports 3000, 5432, 6379, 9000, 9001 - Internal/VPN only - -#### SSL/TLS Setup -1. Obtain SSL certificates (Let's Encrypt, commercial CA) -2. Configure nginx for HTTPS in the frontend container -3. Update APP_BASE_URL to use https:// - -#### Regular Maintenance -- Monitor logs: `docker-compose -f docker-compose.deploy.yml logs -f` -- Update images periodically -- Backup database and MinIO data -- Monitor disk space and performance - -## Management Commands - -### View Logs -```cmd -# All services -docker-compose -f docker-compose.deploy.yml logs -f - -# Specific service -docker-compose -f docker-compose.deploy.yml logs -f backend -``` - -### Restart Services -```cmd -# Restart all -docker-compose -f docker-compose.deploy.yml restart - -# Restart specific service -docker-compose -f docker-compose.deploy.yml restart backend -``` - -### Stop Services -```cmd -docker-compose -f docker-compose.deploy.yml down -``` - -### Update Deployment -1. Stop current services -2. Load new images -3. Start services with new configuration - -## Backup Strategy - -### Database Backup -```cmd -docker exec serpentrace-postgres pg_dump -U postgres serpentrace > backup_$(date +%Y%m%d).sql -``` - -### Complete Backup -```cmd -# Stop services -docker-compose -f docker-compose.deploy.yml down - -# Backup volumes -docker run --rm -v postgres_data:/data -v %cd%:/backup ubuntu tar czf /backup/postgres_backup.tar.gz -C /data . -docker run --rm -v minio_data:/data -v %cd%:/backup ubuntu tar czf /backup/minio_backup.tar.gz -C /data . - -# Restart services -docker-compose -f docker-compose.deploy.yml up -d -``` - -## Troubleshooting - -### Common Issues - -#### Services Not Starting -1. Check Docker is running -2. Verify port availability -3. Check environment variables -4. Review logs for specific errors - -#### Database Connection Issues -1. Verify POSTGRES_PASSWORD matches in .env.server -2. Check database container is healthy -3. Ensure network connectivity - -#### Frontend Not Loading -1. Check nginx container status -2. Verify backend API is responding -3. Check browser console for errors - -#### Performance Issues -1. Monitor resource usage: `docker stats` -2. Check available disk space -3. Review application logs -4. Consider scaling if needed - -### Getting Help -- Check application logs for specific error messages -- Verify all environment variables are set correctly -- Ensure all required ports are available -- Contact support with log files and configuration details - -## Version Information -- SerpentRace Backend: Latest -- Frontend: Latest -- PostgreSQL: 15-alpine -- Redis: 7-alpine -- MinIO: Latest -- Nginx: Alpine \ No newline at end of file diff --git a/SerpentRace_Docker/deployment/docker-compose.deploy.yml b/SerpentRace_Docker/deployment/docker-compose.deploy.yml deleted file mode 100644 index 5975ec0d..00000000 --- a/SerpentRace_Docker/deployment/docker-compose.deploy.yml +++ /dev/null @@ -1,149 +0,0 @@ -version: '3.8' - -services: - # Backend service using pre-built image - backend: - image: serpentrace_docker-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_docker-frontend:latest - container_name: serpentrace-frontend - restart: unless-stopped - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - 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 - -volumes: - postgres_data: - driver: local - redis_data: - driver: local - minio_data: - driver: local - backend_logs: - driver: local - -networks: - serpentrace-network: - driver: bridge \ No newline at end of file diff --git a/SerpentRace_Docker/deployment/load-images.bat b/SerpentRace_Docker/deployment/load-images.bat deleted file mode 100644 index 583eea00..00000000 --- a/SerpentRace_Docker/deployment/load-images.bat +++ /dev/null @@ -1,103 +0,0 @@ -@echo off -REM SerpentRace Production Deployment Script -REM This script loads Docker images and starts the production environment - -setlocal EnableDelayedExpansion - -echo =============================================== -echo SerpentRace Production Deployment -echo =============================================== -echo. - -REM Check if Docker is installed -where docker >nul 2>nul -if %errorlevel% neq 0 ( - echo [ERROR] Docker is not installed. Please install Docker first. - pause - exit /b 1 -) - -where docker-compose >nul 2>nul -if %errorlevel% neq 0 ( - echo [ERROR] Docker Compose is not installed. Please install Docker Compose first. - pause - exit /b 1 -) - -REM Check if serpentRaceDocker.tar exists -if not exist "serpentRaceDocker.tar" ( - echo [ERROR] serpentRaceDocker.tar not found! - echo Please ensure the tar file is in the same directory as this script. - pause - exit /b 1 -) - -REM Check if environment file exists -if not exist ".env.server" ( - echo [ERROR] .env.server file not found! - echo Please ensure the environment file is configured. - pause - exit /b 1 -) - -echo [INFO] Loading Docker images from serpentRaceDocker.tar... -docker load -i serpentRaceDocker.tar -if %errorlevel% neq 0 ( - echo [ERROR] Failed to load Docker images! - pause - exit /b 1 -) - -echo [INFO] Images loaded successfully! -echo. - -REM Show loaded images -echo [INFO] Loaded images: -docker images | findstr serpentrace -docker images | findstr postgres -docker images | findstr redis -docker images | findstr minio -echo. - -echo [WARNING] Before starting the services, please review and update .env.server: -echo - Change all placeholder passwords -echo - Configure email settings -echo - Update domain names -echo - Set strong JWT secret -echo. -echo Press any key to continue with deployment or Ctrl+C to exit... -pause >nul - -echo [INFO] Starting production services... -docker-compose -f docker-compose.deploy.yml --env-file .env.server up -d - -if %errorlevel% neq 0 ( - echo [ERROR] Failed to start services! - pause - exit /b 1 -) - -echo. -echo =============================================== -echo Deployment Complete! -echo =============================================== -echo. -echo Services are starting up. Please wait a few moments for all services to be ready. -echo. -echo Available services: -echo - Frontend: http://localhost (or your domain) -echo - Backend API: http://localhost:3000 -echo - MinIO Console: http://localhost:9001 -echo. -echo To check service status: docker-compose -f docker-compose.deploy.yml ps -echo To view logs: docker-compose -f docker-compose.deploy.yml logs -f [service_name] -echo To stop services: docker-compose -f docker-compose.deploy.yml down -echo. -echo IMPORTANT SECURITY NOTES: -echo 1. Change all default passwords in .env.server -echo 2. Configure firewall rules for your server -echo 3. Set up SSL/TLS certificates for HTTPS -echo 4. Configure regular backups -echo 5. Monitor logs and system resources -echo. -pause \ No newline at end of file diff --git a/SerpentRace_Docker/deployment/load-images.sh b/SerpentRace_Docker/deployment/load-images.sh deleted file mode 100644 index 481949ee..00000000 --- a/SerpentRace_Docker/deployment/load-images.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/bin/bash -# SerpentRace Production Deployment Script for Linux -# This script loads Docker images and starts the production environment - -set -e - -echo "===============================================" -echo "SerpentRace Production Deployment" -echo "===============================================" -echo - -# Check if Docker is installed -if ! command -v docker &> /dev/null; then - echo "[ERROR] Docker is not installed. Please install Docker first." - exit 1 -fi - -if ! command -v docker-compose &> /dev/null; then - echo "[ERROR] Docker Compose is not installed. Please install Docker Compose first." - exit 1 -fi - -# Check if serpentRaceDocker.tar exists -if [ ! -f "serpentRaceDocker.tar" ]; then - echo "[ERROR] serpentRaceDocker.tar not found!" - echo "Please ensure the tar file is in the same directory as this script." - exit 1 -fi - -# Check if environment file exists -if [ ! -f ".env.server" ]; then - echo "[ERROR] .env.server file not found!" - echo "Please ensure the environment file is configured." - exit 1 -fi - -echo "[INFO] Loading Docker images from serpentRaceDocker.tar..." -docker load -i serpentRaceDocker.tar - -echo "[INFO] Images loaded successfully!" -echo - -# Show loaded images -echo "[INFO] Loaded images:" -docker images | grep -E "(serpentrace|postgres|redis|minio)" -echo - -echo "[WARNING] Before starting the services, please review and update .env.server:" -echo " - Change all placeholder passwords" -echo " - Configure email settings" -echo " - Update domain names" -echo " - Set strong JWT secret" -echo -read -p "Press Enter to continue with deployment or Ctrl+C to exit..." - -echo "[INFO] Starting production services..." -docker-compose -f docker-compose.deploy.yml --env-file .env.server up -d - -echo -echo "===============================================" -echo "Deployment Complete!" -echo "===============================================" -echo -echo "Services are starting up. Please wait a few moments for all services to be ready." -echo -echo "Available services:" -echo " - Frontend: http://localhost (or your domain)" -echo " - Backend API: http://localhost:3000" -echo " - MinIO Console: http://localhost:9001" -echo -echo "To check service status: docker-compose -f docker-compose.deploy.yml ps" -echo "To view logs: docker-compose -f docker-compose.deploy.yml logs -f [service_name]" -echo "To stop services: docker-compose -f docker-compose.deploy.yml down" -echo -echo "IMPORTANT SECURITY NOTES:" -echo "1. Change all default passwords in .env.server" -echo "2. Configure firewall rules for your server" -echo "3. Set up SSL/TLS certificates for HTTPS" -echo "4. Configure regular backups" -echo "5. Monitor logs and system resources" -echo \ No newline at end of file diff --git a/SerpentRace_Docker/deployment/nginx.conf b/SerpentRace_Docker/deployment/nginx.conf deleted file mode 100644 index fa4630a8..00000000 --- a/SerpentRace_Docker/deployment/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/deployment/pgadmin_servers_deployment.json b/SerpentRace_Docker/deployment/pgadmin_servers_deployment.json deleted file mode 100644 index a77d806e..00000000 --- a/SerpentRace_Docker/deployment/pgadmin_servers_deployment.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Servers": { - "1": { - "Name": "SerpentRace Production", - "Group": "Servers", - "Host": "postgres", - "Port": 5432, - "MaintenanceDB": "serpentrace", - "Username": "postgres", - "SSLMode": "prefer", - "Comment": "SerpentRace Production Database" - } - } -} diff --git a/SerpentRace_Docker/deployment/serpentRaceDocker.tar b/SerpentRace_Docker/deployment/serpentRaceDocker.tar deleted file mode 100644 index a4a84819..00000000 Binary files a/SerpentRace_Docker/deployment/serpentRaceDocker.tar and /dev/null differ diff --git a/SerpentRace_Docker/deployment/sql_schema_only.sql b/SerpentRace_Docker/deployment/sql_schema_only.sql deleted file mode 100644 index e36d9e25..00000000 --- a/SerpentRace_Docker/deployment/sql_schema_only.sql +++ /dev/null @@ -1,236 +0,0 @@ --- SerpentRace Database Schema --- Generated from TypeORM Entity Aggregates --- This file creates the complete database schema without initial data - --- Enable UUID extension -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- Create Users table -CREATE TABLE "Users" ( - "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "orgid" UUID NULL, - "username" VARCHAR(100) UNIQUE NOT NULL, - "password" VARCHAR(255) NOT NULL, - "email" VARCHAR(255) UNIQUE NOT NULL, - "fname" VARCHAR(100) NOT NULL, - "lname" VARCHAR(100) NOT NULL, - "token" VARCHAR(255) NULL, - "TokenExpires" TIMESTAMP NULL, - "phone" VARCHAR(20) NULL, - "state" INTEGER NOT NULL DEFAULT 0, - "regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "Orglogindate" TIMESTAMP NULL -); - --- Create Organizations table -CREATE TABLE "Organizations" ( - "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "name" VARCHAR(255) NOT NULL, - "contactfname" VARCHAR(100) NOT NULL, - "contactlname" VARCHAR(100) NOT NULL, - "contactphone" VARCHAR(20) NOT NULL, - "contactemail" VARCHAR(255) NOT NULL, - "state" INTEGER NOT NULL DEFAULT 0, - "regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "url" VARCHAR(500) NULL, - "userinorg" INTEGER NOT NULL DEFAULT 0, - "maxOrganizationalDecks" INTEGER NULL -); - --- Create Decks table -CREATE TABLE "Decks" ( - "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "name" VARCHAR(255) NOT NULL, - "type" INTEGER NOT NULL, - "user_id" UUID NOT NULL, - "creation_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "cards" JSONB NOT NULL DEFAULT '[]', - "played_number" INTEGER NOT NULL DEFAULT 0, - "ctype" INTEGER NOT NULL DEFAULT 0, - "update_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "state" INTEGER NOT NULL DEFAULT 0, - "organization_id" UUID NULL -); - --- Create Chats table -CREATE TABLE "Chats" ( - "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "type" VARCHAR(50) NOT NULL DEFAULT 'direct', - "name" VARCHAR(255) NULL, - "gameId" UUID NULL, - "createdBy" UUID NULL, - "users" UUID[] NOT NULL, - "messages" JSONB NOT NULL DEFAULT '[]', - "lastActivity" TIMESTAMP NULL, - "createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "state" INTEGER NOT NULL DEFAULT 0, - "archiveDate" TIMESTAMP NULL -); - --- Create Contacts table -CREATE TABLE "Contacts" ( - "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "name" VARCHAR(255) NOT NULL, - "email" VARCHAR(255) NOT NULL, - "userid" UUID NULL, - "type" INTEGER NOT NULL, - "txt" TEXT NOT NULL, - "state" INTEGER NOT NULL DEFAULT 0, - "createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "adminResponse" TEXT NULL, - "responseDate" TIMESTAMP NULL, - "respondedBy" UUID NULL -); - --- Create Games table -CREATE TABLE "Games" ( - "id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - "gamecode" VARCHAR(10) UNIQUE NOT NULL, - "maxplayers" INTEGER NOT NULL, - "logintype" INTEGER NOT NULL DEFAULT 0, - "state" INTEGER NOT NULL DEFAULT 0, - "playerids" UUID[] NOT NULL DEFAULT '{}', - "decks" JSONB NOT NULL DEFAULT '[]', - "boardsize" INTEGER NOT NULL DEFAULT 50, - "createDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "finishDate" TIMESTAMP NULL, - "winnerid" UUID NULL, - "createdBy" UUID NOT NULL, - "organizationid" UUID NULL -); - --- Add Foreign Key Constraints -ALTER TABLE "Users" -ADD CONSTRAINT "FK_Users_Organizations" -FOREIGN KEY ("orgid") REFERENCES "Organizations"("id") ON DELETE SET NULL; - -ALTER TABLE "Decks" -ADD CONSTRAINT "FK_Decks_Users" -FOREIGN KEY ("user_id") REFERENCES "Users"("id") ON DELETE CASCADE; - -ALTER TABLE "Decks" -ADD CONSTRAINT "FK_Decks_Organizations" -FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE SET NULL; - -ALTER TABLE "Contacts" -ADD CONSTRAINT "FK_Contacts_Users" -FOREIGN KEY ("userid") REFERENCES "Users"("id") ON DELETE SET NULL; - -ALTER TABLE "Contacts" -ADD CONSTRAINT "FK_Contacts_RespondedBy" -FOREIGN KEY ("respondedBy") REFERENCES "Users"("id") ON DELETE SET NULL; - -ALTER TABLE "Chats" -ADD CONSTRAINT "FK_Chats_CreatedBy" -FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE SET NULL; - -ALTER TABLE "Chats" -ADD CONSTRAINT "FK_Chats_Games" -FOREIGN KEY ("gameId") REFERENCES "Games"("id") ON DELETE SET NULL; - -ALTER TABLE "Games" -ADD CONSTRAINT "FK_Games_CreatedBy" -FOREIGN KEY ("createdBy") REFERENCES "Users"("id") ON DELETE CASCADE; - -ALTER TABLE "Games" -ADD CONSTRAINT "FK_Games_Organizations" -FOREIGN KEY ("organizationid") REFERENCES "Organizations"("id") ON DELETE SET NULL; - -ALTER TABLE "Games" -ADD CONSTRAINT "FK_Games_Winner" -FOREIGN KEY ("winnerid") REFERENCES "Users"("id") ON DELETE SET NULL; - --- Create Indexes for Performance -CREATE INDEX "IDX_Users_Username" ON "Users" ("username"); -CREATE INDEX "IDX_Users_Email" ON "Users" ("email"); -CREATE INDEX "IDX_Users_OrgId" ON "Users" ("orgid"); -CREATE INDEX "IDX_Users_State" ON "Users" ("state"); - -CREATE INDEX "IDX_Organizations_Name" ON "Organizations" ("name"); -CREATE INDEX "IDX_Organizations_State" ON "Organizations" ("state"); - -CREATE INDEX "IDX_Decks_UserId" ON "Decks" ("user_id"); -CREATE INDEX "IDX_Decks_Type" ON "Decks" ("type"); -CREATE INDEX "IDX_Decks_CType" ON "Decks" ("ctype"); -CREATE INDEX "IDX_Decks_State" ON "Decks" ("state"); -CREATE INDEX "IDX_Decks_OrganizationId" ON "Decks" ("organization_id"); - -CREATE INDEX "IDX_Chats_Type" ON "Chats" ("type"); -CREATE INDEX "IDX_Chats_State" ON "Chats" ("state"); -CREATE INDEX "IDX_Chats_GameId" ON "Chats" ("gameId"); -CREATE INDEX "IDX_Chats_CreatedBy" ON "Chats" ("createdBy"); - -CREATE INDEX "IDX_Contacts_Type" ON "Contacts" ("type"); -CREATE INDEX "IDX_Contacts_State" ON "Contacts" ("state"); -CREATE INDEX "IDX_Contacts_UserId" ON "Contacts" ("userid"); - -CREATE INDEX "IDX_Games_GameCode" ON "Games" ("gamecode"); -CREATE INDEX "IDX_Games_State" ON "Games" ("state"); -CREATE INDEX "IDX_Games_CreatedBy" ON "Games" ("createdBy"); -CREATE INDEX "IDX_Games_OrganizationId" ON "Games" ("organizationid"); - --- Create update trigger for updatedate columns -CREATE OR REPLACE FUNCTION update_updatedate_column() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updatedate = CURRENT_TIMESTAMP; - RETURN NEW; -END; -$$ language 'plpgsql'; - --- Apply update triggers -CREATE TRIGGER update_users_updatedate - BEFORE UPDATE ON "Users" - FOR EACH ROW EXECUTE FUNCTION update_updatedate_column(); - -CREATE TRIGGER update_organizations_updatedate - BEFORE UPDATE ON "Organizations" - FOR EACH ROW EXECUTE FUNCTION update_updatedate_column(); - -CREATE TRIGGER update_decks_updatedate - BEFORE UPDATE ON "Decks" - FOR EACH ROW EXECUTE FUNCTION update_updatedate_column(); - -CREATE TRIGGER update_chats_updatedate - BEFORE UPDATE ON "Chats" - FOR EACH ROW EXECUTE FUNCTION update_updatedate_column(); - -CREATE TRIGGER update_contacts_updatedate - BEFORE UPDATE ON "Contacts" - FOR EACH ROW EXECUTE FUNCTION update_updatedate_column(); - -CREATE TRIGGER update_games_updatedate - BEFORE UPDATE ON "Games" - FOR EACH ROW EXECUTE FUNCTION update_updatedate_column(); - --- Comments for documentation -COMMENT ON TABLE "Users" IS 'User accounts with authentication and profile information'; -COMMENT ON TABLE "Organizations" IS 'Organizations that can have multiple users and premium features'; -COMMENT ON TABLE "Decks" IS 'Card decks for the game, can be public, private, or organizational'; -COMMENT ON TABLE "Chats" IS 'Chat system supporting direct messages, groups, and game chats'; -COMMENT ON TABLE "Contacts" IS 'Contact form submissions and support tickets'; -COMMENT ON TABLE "Games" IS 'Game sessions with players, decks, and game state'; - --- Enum value comments -COMMENT ON COLUMN "Users"."state" IS '0=REGISTERED_NOT_VERIFIED, 1=VERIFIED_REGULAR, 2=VERIFIED_PREMIUM, 3=SOFT_DELETE, 4=DEACTIVATED, 5=ADMIN'; -COMMENT ON COLUMN "Organizations"."state" IS '0=REGISTERED, 1=ACTIVE, 2=SOFT_DELETE'; -COMMENT ON COLUMN "Decks"."type" IS '0=LUCK, 1=JOKER, 2=QUESTION'; -COMMENT ON COLUMN "Decks"."ctype" IS '0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION'; -COMMENT ON COLUMN "Decks"."state" IS '0=ACTIVE, 1=SOFT_DELETE'; -COMMENT ON COLUMN "Chats"."type" IS 'direct, group, game'; -COMMENT ON COLUMN "Chats"."state" IS '0=ACTIVE, 1=ARCHIVE, 2=SOFT_DELETE'; -COMMENT ON COLUMN "Contacts"."type" IS '0=BUG, 1=PROBLEM, 2=QUESTION, 3=SALES, 4=OTHER'; -COMMENT ON COLUMN "Contacts"."state" IS '0=ACTIVE, 1=RESOLVED, 2=SOFT_DELETE'; -COMMENT ON COLUMN "Games"."state" IS '0=WAITING, 1=ACTIVE, 2=FINISHED, 3=CANCELLED'; -COMMENT ON COLUMN "Games"."logintype" IS '0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION'; - --- Grant permissions for application user --- Note: Replace 'serpentrace_app' with your actual application database user --- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO serpentrace_app; --- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO serpentrace_app; --- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO serpentrace_app; \ No newline at end of file diff --git a/deployment/docker-compose.deploy.yml b/deployment/docker-compose.deploy.yml index 5975ec0d..4567e3f9 100644 --- a/deployment/docker-compose.deploy.yml +++ b/deployment/docker-compose.deploy.yml @@ -3,36 +3,13 @@ version: '3.8' services: # Backend service using pre-built image backend: - image: serpentrace_docker-backend:latest + 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} + env_file: + - .env.server volumes: - backend_logs:/app/logs depends_on: @@ -44,20 +21,15 @@ services: condition: service_healthy networks: - serpentrace-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s + tty: true # Frontend service using pre-built image frontend: - image: serpentrace_docker-frontend:latest + image: serpentrace-frontend:latest container_name: serpentrace-frontend restart: unless-stopped ports: - - "80:80" + - "8080:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro @@ -81,7 +53,7 @@ services: environment: POSTGRES_DB: serpentrace POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: postgres POSTGRES_INITDB_ARGS: "--encoding=UTF-8" volumes: - postgres_data:/var/lib/postgresql/data @@ -103,7 +75,7 @@ services: - "6379:6379" volumes: - redis_data:/data - command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} + command: redis-server --appendonly yes networks: - serpentrace-network healthcheck: @@ -121,8 +93,8 @@ services: - "9000:9000" - "9001:9001" environment: - MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} - MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} + MINIO_ROOT_USER: serpentrace + MINIO_ROOT_PASSWORD: serpentrace123! volumes: - minio_data:/data command: server /data --console-address ":9001" diff --git a/serpentRaceDocker.tar b/serpentRaceDocker.tar deleted file mode 100644 index 3089737d..00000000 Binary files a/serpentRaceDocker.tar and /dev/null differ